diff --git a/nodemon.json b/nodemon.json
new file mode 100644
index 00000000..374a32b6
--- /dev/null
+++ b/nodemon.json
@@ -0,0 +1,21 @@
+{
+ "ignoreRoot": [
+ ".git",
+ ".nyc_output",
+ ".sass-cache",
+ "bower_components",
+ "coverage",
+ "./node_modules/!(q-templates-application)/dist/template.json"
+ ],
+ "watch": [
+ "node_modules/q-templates-application/dist/template.json",
+ "db",
+ "docs",
+ "middleware",
+ "openapi",
+ "public",
+ "questionnaire",
+ "services",
+ "app.js"
+ ]
+}
diff --git a/package-lock.json b/package-lock.json
index 62e7c367..47041e57 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,8 +31,9 @@
"notifications-node-client": "^5.1.0",
"pg": "^8.7.3",
"pino-http": "^5.5.0",
+ "q-expressions": "github:CriminalInjuriesCompensationAuthority/q-expressions#v1.0.0",
"q-router": "github:CriminalInjuriesCompensationAuthority/q-router#v3.0.0",
- "q-templates-application": "github:CriminalInjuriesCompensationAuthority/q-templates-application#v8.0.3",
+ "q-templates-application": "github:CriminalInjuriesCompensationAuthority/q-templates-application#rc-personalised-notifications",
"swagger-ui-express": "^4.3.0",
"uuid": "^3.3.2",
"verror": "^1.10.0"
@@ -9832,6 +9833,19 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
},
+ "node_modules/q-expressions": {
+ "version": "1.0.0",
+ "resolved": "git+ssh://git@github.com/CriminalInjuriesCompensationAuthority/q-expressions.git#f1545970b4863b22b1df9533e462f5b0706d92c4",
+ "license": "MIT",
+ "dependencies": {
+ "json-rules": "github:CriminalInjuriesCompensationAuthority/json-rules#v1.0.0",
+ "moment": "^2.24.0"
+ },
+ "engines": {
+ "node": ">=16.0.0",
+ "npm": ">=8.5.2"
+ }
+ },
"node_modules/q-router": {
"version": "3.0.0",
"resolved": "git+ssh://git@github.com/CriminalInjuriesCompensationAuthority/q-router.git#b18d9e66854915f2642aaa0b0bd2a6aaa6bb0935",
@@ -9846,8 +9860,8 @@
}
},
"node_modules/q-templates-application": {
- "version": "8.0.3",
- "resolved": "git+ssh://git@github.com/CriminalInjuriesCompensationAuthority/q-templates-application.git#780bde6f746175a162f90999917d84a5b14da560",
+ "version": "8.0.4",
+ "resolved": "git+ssh://git@github.com/CriminalInjuriesCompensationAuthority/q-templates-application.git#2890cd02b70d2b5ad06e7e05ba7138e711007230",
"license": "MIT",
"engines": {
"node": ">=16.0.0",
diff --git a/package.json b/package.json
index 53c3a6e3..9f20cf16 100644
--- a/package.json
+++ b/package.json
@@ -7,9 +7,9 @@
},
"scripts": {
"start": "node ./bin/www",
- "start:dev": "nodemon -L --inspect=0.0.0.0:9229 -e .js,.json,.njk,.yml --ignore openapi/openapi.json --exec npm run build:run",
+ "start:dev": "nodemon -L -e .js,.json,.njk,.yml --ignore openapi/openapi.json --exec npm run build:run:dev",
"openapi:build": "speccy lint openapi/src/openapi-src.json -j && speccy resolve ./openapi/src/openapi-src.json -j | yaml2json --pretty --indentation 4 --save - > ./openapi/openapi.json && node ./openapi/src/dereference-openapi.js",
- "build:run": "npm run openapi:build && npm run start",
+ "build:run:dev": "npm run openapi:build && node --inspect=0.0.0.0:9229 ./bin/www",
"pretestx": "eslint .",
"test": "jest",
"coverage": "jest --coverage",
@@ -40,8 +40,9 @@
"notifications-node-client": "^5.1.0",
"pg": "^8.7.3",
"pino-http": "^5.5.0",
+ "q-expressions": "github:CriminalInjuriesCompensationAuthority/q-expressions#v1.0.0",
"q-router": "github:CriminalInjuriesCompensationAuthority/q-router#v3.0.0",
- "q-templates-application": "github:CriminalInjuriesCompensationAuthority/q-templates-application#v8.0.3",
+ "q-templates-application": "github:CriminalInjuriesCompensationAuthority/q-templates-application#rc-personalised-notifications",
"swagger-ui-express": "^4.3.0",
"uuid": "^3.3.2",
"verror": "^1.10.0"
diff --git a/questionnaire/dataset/dataset-service.declaration.test.js b/questionnaire/dataset/dataset-service.declaration.test.js
index 80860779..c5be9f35 100644
--- a/questionnaire/dataset/dataset-service.declaration.test.js
+++ b/questionnaire/dataset/dataset-service.declaration.test.js
@@ -156,10 +156,9 @@ describe('Dataset service', () => {
id: 'q-applicant-declaration',
type: 'simple',
label:
- '
By submitting this application, I, Mr Foo Bar, confirm that I understand the following:
- the information I’ve given here is true
- CICA may share and receive information with the following parties for the purposes of processing this application for compensation or verifying information provided:
- police, prosecutors and ACRO Criminal Records Office, including for the purposes of obtaining a report of the crime and a record of any criminal convictions I may have
- medical organisations, practitioners, and police medical staff to obtain medical evidence - including medical records and expert reports. CICA will let me know if this is required
- any other individuals or organisations where necessary to process this application
- any representative I may appoint to act for me in the course of this application
- if I deliberately provide information that I know is wrong or misleading, I may be refused compensation and may be prosecuted
- I must notify CICA immediately of any change in circumstances relevant to this application, including my address and information about any other claim or proceedings which may give rise to a separate award or payment in respect of my injuries
Read our privacy notice to see how we use your data (opens in new tab).
Information about appointing a legal or another representative
It is not necessary to appoint a legal or other paid representative to act on an applicant’s behalf. If one is appointed at any stage, please be aware that CICA cannot meet their costs. We will communicate directly with any appointed representative.
If we make an award, we will pay it only to an applicant or their legal representative. This is unless the application has been made on behalf of an adult who cannot manage their own financial affairs.
If it is decided that a representative’s services are no longer required, you must tell us in writing as soon as possible. If a monetary award is to be made and there is a dispute about outstanding legal fees, it is our policy to retain the disputed amount until the parties involved resolve the dispute.
',
+ ' You have told us that you are Mr Foo Bar and you are applying on behalf of yourself.
By submitting this application, you confirm that you understand the following:
- the information given in this application for compensation is true
- Criminal Injuries Compensation Authority (CICA) may share and receive information with the following parties for the purposes of processing this application for compensation or verifying information provided:
- police, prosecutors and ACRO Criminal Records Office, including for the purposes of obtaining a report of the crime and a record of any criminal convictions you may have
- medical organisations, practitioners, and police medical staff to obtain medical evidence – including medical records and expert reports. CICA will let you know if this is required
- any other individuals or organisations where necessary to process this application
- any representative appointed to act for you in the course of this application
- CICA must be notified immediately of any change in circumstances relevant to this application, including any change of address and information about any other claim or proceedings which may give rise to a separate award or payment in respect of your injuries
Providing wrong or misleading information
If untrue or misleading information is deliberately provided, compensation may be refused and the person(s) responsible may be prosecuted.
Read our privacy notice to see how we use your data (opens in new tab).
Information about appointing a legal or another representative
It is not necessary to appoint a legal or other representative to act on a victim’s behalf. If a representative is appointed at any stage, please be aware that:
- CICA cannot meet their costs
- we will only communicate directly with any appointed representative
If we make an award, we will pay it only to the victim or their legal representative. This is unless the application has been made on behalf of:
- an adult who cannot manage their own financial affairs
- a child who is under 18 years of age
It is our general policy to put an award for a child in an interest-earning deposit account until they reach the age of 18.
If a monetary award is to be made and there is a dispute about outstanding legal fees, it is our policy to retain the disputed amount until the dispute has been resolved.
If it is decided that a representative’s services are no longer required, CICA must be notified in writing as soon as possible.
',
value: 'i-agree',
- valueLabel:
- 'I have read and understood the information and declaration'
+ valueLabel: 'I have read and understood the declaration'
}
]
}
@@ -219,10 +218,9 @@ describe('Dataset service', () => {
id: 'q-mainapplicant-declaration',
type: 'simple',
label:
- 'By submitting this application, I, Mrs Biz Baz on behalf of Mr Foo Bar confirm that I understand the following:
- the information I’ve given here is true
- CICA may share and receive information with the following parties for the purposes of processing this application for compensation or verifying information provided:
- police, prosecutors and ACRO Criminal Records Office, including for the purposes of obtaining a report of the crime and a record of any criminal convictions they may have
- medical organisations, practitioners, and police medical staff to obtain medical evidence - including medical records and expert reports. CICA will let me know if this is required
- any other individuals or organisations where necessary to process this application
- any representative I may appoint to act for me in the course of this application
- if I deliberately provide information that I know is wrong or misleading, I may be refused compensation and I may be prosecuted
- I must notify CICA immediately of any change in circumstances relevant to this application, including my address and information about any other claim or proceedings which may give rise to a separate award or payment in respect of their injuries
Read our privacy notice to see how we use your data (opens in new tab).
Information about appointing a legal or another representative
It is not necessary to appoint a legal or other representative to act on an applicant’s behalf. If one is appointed at any stage, please be aware that CICA cannot meet their costs. We will communicate directly with any appointed representative.
If we make an award, we will pay it only to an applicant or their legal representative. This is unless the application has been made on behalf of an adult who cannot manage their own financial affairs or a child who is under 18 years of age. It is our general policy to put an award for a child in an interest earning deposit account until they reach the age of 18.
If it is decided that a representative’s services are no longer required, you must tell us in writing as soon as possible. If a monetary award is to be made and there is a dispute about outstanding legal fees, it is our policy to retain the disputed amount until the parties involved resolve the dispute.
',
+ ' You have told us that you are Mrs Biz Baz and you are applying on behalf of Mr Foo Bar (the victim).
By submitting this application, you confirm that you understand the following:
- the information given in this application for compensation is true
- Criminal Injuries Compensation Authority (CICA) may share and receive information with the following parties for the purposes of processing this application for compensation or verifying information provided:
- police, prosecutors and ACRO Criminal Records Office, including for the purposes of obtaining a report of the crime and a record of any criminal convictions the victim may have
- medical organisations, practitioners, and police medical staff to obtain medical evidence – including medical records and expert reports. CICA will let you know if this is required
- any other individuals or organisations where necessary to process this application
- any representative appointed to act for the victim in the course of this application
- CICA must be notified immediately of any change in circumstances relevant to this application, including any change of address and information about any other claim or proceedings which may give rise to a separate award or payment in respect of the victim’s Injuries
Providing wrong or misleading information
If untrue or misleading information is deliberately provided, compensation may be refused and the person(s) responsible may be prosecuted.
Read our privacy notice to see how we use your data (opens in new tab).
Information about appointing a legal or another representative
It is not necessary to appoint a legal or other representative to act on a victim’s behalf. If a representative is appointed at any stage, please be aware that:
- CICA cannot meet their costs
- we will only communicate directly with any appointed representative
If we make an award, we will pay it only to the victim or their legal representative. This is unless the application has been made on behalf of:
- an adult who cannot manage their own financial affairs
- a child who is under 18 years of age
It is our general policy to put an award for a child in an interest-earning deposit account until they reach the age of 18.
If a monetary award is to be made and there is a dispute about outstanding legal fees, it is our policy to retain the disputed amount until the dispute has been resolved.
If it is decided that a representative’s services are no longer required, CICA must be notified in writing as soon as possible.
',
value: 'i-agree-under-12',
- valueLabel:
- 'I have read and understood the information and declaration'
+ valueLabel: 'I have read and understood the declaration'
}
]
}
@@ -282,10 +280,9 @@ describe('Dataset service', () => {
id: 'q-mainapplicant-declaration',
type: 'simple',
label:
- 'By submitting this application, I, Dr Waldo Fred, confirm that Mr Foo Bar understands the following:
- the information I’ve given here is true
- CICA may share and receive information with the following parties for the purposes of processing this application for compensation or verifying information provided:
- police, prosecutors and ACRO Criminal Records Office, including for the purposes of obtaining a report of the crime and a record of any criminal convictions they may have
- medical organisations, practitioners, and police medical staff to obtain medical evidence - including medical records and expert reports. CICA will let me know if this is required
- any other individuals or organisations where necessary to process this application
- any representative I may appoint to act for me in the course of this application
- if we deliberately provide information that we know is wrong or misleading, we may be refused compensation and we may be prosecuted
- we must notify CICA immediately of any change in circumstances relevant to this application, including my address and information about any other claim or proceedings which may give rise to a separate award or payment in respect of their injuries
Read our privacy notice to see how we use your data (opens in new tab).
Information about appointing a legal or another representative
It is not necessary to appoint a legal or other representative to act on an applicant’s behalf. If one is appointed at any stage, please be aware that CICA cannot meet their costs. We will communicate directly with any appointed representative.
If we make an award, we will pay it only to an applicant or their legal representative. This is unless the application has been made on behalf of an adult who cannot manage their own financial affairs or a child who is under 18 years of age. It is our general policy to put an award for a child in an interest earning deposit account until they reach the age of 18.
If it is decided that a representative’s services are no longer required, you must tell us in writing as soon as possible. If a monetary award is to be made and there is a dispute about outstanding legal fees, it is our policy to retain the disputed amount until the parties involved resolve the dispute.
',
+ ' You have told us that you are Dr Waldo Fred and you are applying on behalf of Mr Foo Bar (the victim).
By submitting this application, you confirm that both you and Mr Foo Bar understand the following:
- the information given in this application for compensation is true
- Criminal Injuries Compensation Authority (CICA) may share and receive information with the following parties for the purposes of processing this application for compensation or verifying information provided:
- police, prosecutors and ACRO Criminal Records Office, including for the purposes of obtaining a report of the crime and a record of any criminal convictions the victim may have
- medical organisations, practitioners, and police medical staff to obtain medical evidence – including medical records and expert reports. CICA will let you know if this is required
- any other individuals or organisations where necessary to process this application
- any representative appointed to act for the victim in the course of this application
- CICA must be notified immediately of any change in circumstances relevant to this application, including any change of address and information about any other claim or proceedings which may give rise to a separate award or payment in respect of the victim’s Injuries
Providing wrong or misleading information
If untrue or misleading information is deliberately provided, compensation may be refused and the person(s) responsible may be prosecuted.
Read our privacy notice to see how we use your data (opens in new tab).
Information about appointing a legal or another representative
It is not necessary to appoint a legal or other representative to act on a victim’s behalf. If a representative is appointed at any stage, please be aware that:
- CICA cannot meet their costs
- we will only communicate directly with any appointed representative
If we make an award, we will pay it only to the victim or their legal representative. This is unless the application has been made on behalf of:
- an adult who cannot manage their own financial affairs
- a child who is under 18 years of age
It is our general policy to put an award for a child in an interest-earning deposit account until they reach the age of 18.
If a monetary award is to be made and there is a dispute about outstanding legal fees, it is our policy to retain the disputed amount until the dispute has been resolved.
If it is decided that a representative’s services are no longer required, CICA must be notified in writing as soon as possible.
',
value: 'i-agree-12-and-over',
- valueLabel:
- 'I have read and understood the information and declaration'
+ valueLabel: 'I have read and understood the declaration'
}
]
}
diff --git a/questionnaire/notifications.test.js b/questionnaire/notifications.test.js
deleted file mode 100644
index dcc6ef53..00000000
--- a/questionnaire/notifications.test.js
+++ /dev/null
@@ -1,307 +0,0 @@
-'use strict';
-
-const VError = require('verror');
-let getQuestionnaireResponse = require('./test-fixtures/res/get_questionnaire.js');
-
-const selfEmailConfirmationQuestionnaireId = 'f4cddbd1-632e-4212-bc4e-debc32e50319';
-const selfSmsConfirmationQuestionnaireId = 'e4522347-dfd3-41d8-a1ad-7119fe92cda5';
-const selfNoConfirmationQuestionnaireId = 'b0c37994-9c62-40f5-bc90-c26cc7ac9d13';
-const minorEmailConfirmationQuestionnaireId = 'a4cddbd1-632e-4212-bc4e-debc32e50319';
-const minorSmsConfirmationQuestionnaireId = 'c4cddbd1-632e-4212-bc4e-debc32e50319';
-
-// the following contain invalid template structures, should be handled via q-validator and q-schema
-const noOnCompleteThrowsErrorQId = 'z4cddbd1-632e-4212-bc4e-debc32e50319';
-const noOnCompleteTasksThrowsErrorQId = 'y4cddbd1-632e-4212-bc4e-debc32e50319';
-const noOnCompleteTasksSendEmailThrowsErrorQId = 'x4cddbd1-632e-4212-bc4e-debc32e50319';
-
-// mock the DAL db integration
-jest.doMock('./questionnaire-dal.js', () =>
- // return a modified factory function, that returns an object with a method, that returns a valid created response
- jest.fn(() => ({
- createQuestionnaire: () => {},
- getQuestionnaire: questionnaireId => {
- // confirmation method is sms.
- // someone_else
- if (questionnaireId === minorSmsConfirmationQuestionnaireId) {
- getQuestionnaireResponse.answers = {
- ...getQuestionnaireResponse.answers,
- 'p-applicant-who-are-you-applying-for': {
- 'q-applicant-who-are-you-applying-for': 'someone-else'
- },
- 'p-mainapplicant-confirmation-method': {
- 'q-mainapplicant-confirmation-method': 'text',
- 'q-mainapplicant-enter-your-telephone-number': '07701234568'
- },
- system: {
- 'case-reference': '44\\444444'
- }
- };
- getQuestionnaireResponse.meta.onComplete.tasks.sendSms.l10n.translations[0].resources = {
- ...getQuestionnaireResponse.meta.onComplete.tasks.sendSms.l10n.translations[0]
- .resources,
- templateId: '0905cf29-054a-4650-9044-a58768fd9381',
- 'templateId_someone-else': 'c2f8f580-3214-4144-bab1-1bbb30863deb',
- phoneNumber:
- '||/answers/p-applicant-confirmation-method/q-applicant-enter-your-telephone-number||',
- 'phoneNumber_someone-else':
- '||/answers/p-mainapplicant-confirmation-method/q-mainapplicant-enter-your-telephone-number||'
- };
- return getQuestionnaireResponse;
- }
- // confirmation method is email.
- // someone_else
- if (questionnaireId === minorEmailConfirmationQuestionnaireId) {
- getQuestionnaireResponse.answers = {
- ...getQuestionnaireResponse.answers,
- 'p-applicant-who-are-you-applying-for': {
- 'q-applicant-who-are-you-applying-for': 'someone-else'
- },
- 'p-mainapplicant-confirmation-method': {
- 'q-mainapplicant-confirmation-method': 'email',
- 'q-mainapplicant-enter-your-email-address': 'someone.for.minor@email.com'
- },
- system: {
- 'case-reference': '22\\222222'
- }
- };
- getQuestionnaireResponse.meta.onComplete.tasks.sendEmail.l10n.translations[0].resources = {
- ...getQuestionnaireResponse.meta.onComplete.tasks.sendEmail.l10n.translations[0]
- .resources,
- templateId: '0a8224c3-9600-4d14-9491-72609dc1dece',
- 'templateId_someone-else': 'b4b08849-c56f-4e82-9f8a-14ab2a50f607',
- emailAddress:
- '||/answers/p-applicant-confirmation-method/q-applicant-enter-your-email-address||',
- 'emailAddress_someone-else':
- '||/answers/p-mainapplicant-confirmation-method/q-mainapplicant-enter-your-email-address||'
- };
- return getQuestionnaireResponse;
- }
- // self email
- if (questionnaireId === selfEmailConfirmationQuestionnaireId) {
- getQuestionnaireResponse.answers = {
- ...getQuestionnaireResponse.answers,
- 'p-applicant-who-are-you-applying-for': {
- 'q-applicant-who-are-you-applying-for': 'myself'
- },
- 'p-applicant-confirmation-method': {
- 'q-applicant-confirmation-method': 'email',
- 'q-applicant-enter-your-email-address': 'somebody@email.com'
- },
- system: {
- 'case-reference': '11\\111111'
- }
- };
- getQuestionnaireResponse.meta.onComplete.tasks.sendEmail.l10n.translations[0].resources = {
- ...getQuestionnaireResponse.meta.onComplete.tasks.sendEmail.l10n.translations[0]
- .resources,
- templateId: '0a8224c3-9600-4d14-9491-72609dc1dece',
- 'templateId_someone-else': 'b4b08849-c56f-4e82-9f8a-14ab2a50f607',
- emailAddress:
- '||/answers/p-applicant-confirmation-method/q-applicant-enter-your-email-address||',
- 'emailAddress_someone-else':
- '||/answers/p-mainapplicant-confirmation-method/q-mainapplicant-enter-your-email-address||'
- };
- return getQuestionnaireResponse;
- }
- // confirmation method is SMS.
- if (questionnaireId === selfSmsConfirmationQuestionnaireId) {
- getQuestionnaireResponse.answers = {
- ...getQuestionnaireResponse.answers,
- 'p-applicant-who-are-you-applying-for': {
- 'q-applicant-who-are-you-applying-for': 'myself'
- },
- 'p-applicant-confirmation-method': {
- 'q-applicant-confirmation-method': 'text',
- 'q-applicant-enter-your-telephone-number': '07701234567'
- },
- system: {
- 'case-reference': '33\\333333'
- }
- };
- getQuestionnaireResponse.meta.onComplete.tasks.sendSms.l10n.translations[0].resources = {
- ...getQuestionnaireResponse.meta.onComplete.tasks.sendSms.l10n.translations[0]
- .resources,
- templateId: '0905cf29-054a-4650-9044-a58768fd9381',
- 'templateId_someone-else': 'c2f8f580-3214-4144-bab1-1bbb30863deb',
- phoneNumber:
- '||/answers/p-applicant-confirmation-method/q-applicant-enter-your-telephone-number||',
- 'phoneNumber_someone-else':
- '||/answers/p-mainapplicant-confirmation-method/q-mainapplicant-enter-your-telephone-number||'
- };
- return getQuestionnaireResponse;
- }
- // confirmation method is "none".
- if (questionnaireId === selfNoConfirmationQuestionnaireId) {
- getQuestionnaireResponse.answers = {
- ...getQuestionnaireResponse.answers,
- 'p-applicant-who-are-you-applying-for': {
- 'q-applicant-who-are-you-applying-for': 'myself'
- },
- 'p-applicant-confirmation-method': {
- 'q-applicant-confirmation-method': 'none'
- },
- system: {
- 'case-reference': '21\\123456'
- }
- };
- return getQuestionnaireResponse;
- }
- // begin invalid template structures
- if (questionnaireId === noOnCompleteThrowsErrorQId) {
- getQuestionnaireResponse.meta = {};
- return getQuestionnaireResponse;
- }
-
- if (questionnaireId === noOnCompleteTasksThrowsErrorQId) {
- getQuestionnaireResponse.meta.onComplete = {};
- return getQuestionnaireResponse;
- }
-
- if (questionnaireId === noOnCompleteTasksSendEmailThrowsErrorQId) {
- getQuestionnaireResponse.meta.onComplete.tasks = {};
- return getQuestionnaireResponse;
- }
-
- // end invalid template structures
- throw new VError(
- {
- name: 'ResourceNotFound'
- },
- `Questionnaire "${questionnaireId}" not found`
- );
- }
- }))
-);
-const mockSendSms = jest.fn();
-const mockSendEmail = jest.fn();
-jest.doMock('../services/notify/index.js', () =>
- jest.fn(() => ({
- sendSms: mockSendSms,
- sendEmail: mockSendEmail
- }))
-);
-
-const createQuestionnaireService = require('./questionnaire-service');
-
-beforeEach(() => {
- jest.resetModules();
- jest.clearAllMocks();
-});
-afterEach(() => {
- // eslint-disable-next-line global-require
- getQuestionnaireResponse = require('./test-fixtures/res/get_questionnaire.js');
-});
-
-describe('Notifications', () => {
- describe('Send confirmation notification for self journeys', () => {
- it('should configure options for an email', async () => {
- const questionnaireService = createQuestionnaireService();
- const response = await questionnaireService.sendConfirmationNotification(
- selfEmailConfirmationQuestionnaireId
- );
- expect(response.emailAddress).toBe('somebody@email.com');
- expect(response).not.toHaveProperty('phoneNumber');
- expect(response.templateId).toBe('0a8224c3-9600-4d14-9491-72609dc1dece');
- expect(response.reference).toBe(null);
- expect(response.personalisation.caseReference).toBe('11\\111111');
- });
- it('should configure options for an SMS', async () => {
- const questionnaireService = createQuestionnaireService();
- const response = await questionnaireService.sendConfirmationNotification(
- selfSmsConfirmationQuestionnaireId
- );
- expect(response).not.toHaveProperty('emailAddress');
- expect(response.phoneNumber).toBe('07701234567');
- expect(response.templateId).toBe('0905cf29-054a-4650-9044-a58768fd9381');
- expect(response.reference).toBe(null);
- expect(response.personalisation.caseReference).toBe('33\\333333');
- });
- it('should NOT configure options for an anything', async () => {
- const questionnaireService = createQuestionnaireService();
- const response = await questionnaireService.sendConfirmationNotification(
- selfNoConfirmationQuestionnaireId
- );
- expect(response).toBe(false);
- });
- it('should send an email', async () => {
- const questionnaireService = createQuestionnaireService();
- await questionnaireService.sendConfirmationNotification(
- selfEmailConfirmationQuestionnaireId
- );
- expect(mockSendEmail).toHaveBeenCalledTimes(1);
- });
- it('should send an SMS', async () => {
- const questionnaireService = createQuestionnaireService();
- await questionnaireService.sendConfirmationNotification(
- selfSmsConfirmationQuestionnaireId
- );
- expect(mockSendSms).toHaveBeenCalledTimes(1);
- });
- it('should NOT send anything', async () => {
- const questionnaireService = createQuestionnaireService();
- await questionnaireService.sendConfirmationNotification(
- selfNoConfirmationQuestionnaireId
- );
- expect(mockSendSms).not.toHaveBeenCalled();
- expect(mockSendEmail).not.toHaveBeenCalled();
- });
- });
- describe('Send confirmation notification for minor journeys', () => {
- it('should configure options for an email', async () => {
- const questionnaireService = createQuestionnaireService();
- const response = await questionnaireService.sendConfirmationNotification(
- minorEmailConfirmationQuestionnaireId
- );
- expect(response.emailAddress).toBe('someone.for.minor@email.com');
- expect(response).not.toHaveProperty('phoneNumber');
- expect(response.reference).toBe(null);
- expect(response.personalisation.caseReference).toBe('22\\222222');
- expect(response.templateId).toBe('b4b08849-c56f-4e82-9f8a-14ab2a50f607');
- });
- it('should configure options for an SMS', async () => {
- const questionnaireService = createQuestionnaireService();
- const response = await questionnaireService.sendConfirmationNotification(
- minorSmsConfirmationQuestionnaireId
- );
- expect(response).not.toHaveProperty('emailAddress');
- expect(response.phoneNumber).toBe('07701234568');
- expect(response.templateId).toBe('c2f8f580-3214-4144-bab1-1bbb30863deb');
- expect(response.personalisation.caseReference).toBe('44\\444444');
- expect(response.reference).toBe(null);
- });
- });
- // these tests should really be handled by q-validator and q-schema changes.
- describe('error handling for send confirmation notification', () => {
- it('should throw an error if meta onComplete not defined ', async () => {
- const mockLogger = {error: jest.fn()};
- const questionnaireService = createQuestionnaireService({logger: mockLogger});
- await questionnaireService.sendConfirmationNotification(noOnCompleteThrowsErrorQId);
- expect(mockSendSms).not.toHaveBeenCalled();
- expect(mockSendEmail).not.toHaveBeenCalled();
- const err = new TypeError("Cannot read properties of undefined (reading 'tasks')");
- expect(mockLogger.error).toHaveBeenCalledWith({err}, 'NOTIFICATION SENDING FAILED');
- });
- it('should throw an error if meta onComplete.tasks not defined ', async () => {
- const mockLogger = {error: jest.fn()};
- const questionnaireService = createQuestionnaireService({logger: mockLogger});
- await questionnaireService.sendConfirmationNotification(
- noOnCompleteTasksThrowsErrorQId
- );
- expect(mockSendSms).not.toHaveBeenCalled();
- expect(mockSendEmail).not.toHaveBeenCalled();
- const err = new TypeError('Cannot convert undefined or null to object');
- expect(mockLogger.error).toHaveBeenCalledWith({err}, 'NOTIFICATION SENDING FAILED');
- });
- it('should throw an error if meta onComplete.tasks.sendEmail not defined ', async () => {
- const mockLogger = {error: jest.fn()};
- const questionnaireService = createQuestionnaireService({logger: mockLogger});
- await questionnaireService.sendConfirmationNotification(
- noOnCompleteTasksSendEmailThrowsErrorQId
- );
- expect(mockSendSms).not.toHaveBeenCalled();
- expect(mockSendEmail).not.toHaveBeenCalled();
- const err = new TypeError("Cannot read properties of undefined (reading 'data')");
- expect(mockLogger.error).toHaveBeenCalledWith({err}, 'NOTIFICATION SENDING FAILED');
- });
- });
-});
diff --git a/questionnaire/questionnaire-service.js b/questionnaire/questionnaire-service.js
index 991fe240..1420edcc 100644
--- a/questionnaire/questionnaire-service.js
+++ b/questionnaire/questionnaire-service.js
@@ -8,14 +8,12 @@ const VError = require('verror');
const createQRouter = require('q-router');
const uuidv4 = require('uuid/v4');
const ajvFormatsMobileUk = require('ajv-formats-mobile-uk');
-const JsonTranslator = require('json-translator');
const templates = require('./templates');
const createMessageBusCaller = require('../services/message-bus');
const createNotifyService = require('../services/notify');
const createSlackService = require('../services/slack');
const questionnaireResource = require('./resources/questionnaire-resource');
const createQuestionnaireHelper = require('./questionnaire/questionnaire');
-const replaceJsonPointers = require('../services/replace-json-pointer/index');
const defaults = {};
defaults.createQuestionnaireDAL = require('./questionnaire-dal');
@@ -134,52 +132,6 @@ function createQuestionnaireService({
}
}
- async function sendConfirmationNotification(questionnaireId) {
- const sharedJsonTranslator = JsonTranslator();
- const jsonTranslator = await sharedJsonTranslator;
- const questionnaire = await getQuestionnaire(questionnaireId);
- try {
- const onCompleteTasks = questionnaire.meta.onComplete.tasks;
-
- Object.keys(onCompleteTasks).forEach(taskName => {
- const confirmationNotificationConfig = onCompleteTasks[taskName];
- const replacedJsonPointersConfig = JSON.parse(
- replaceJsonPointers(
- JSON.stringify(confirmationNotificationConfig),
- questionnaire
- )
- );
- onCompleteTasks[taskName].data = JSON.parse(
- jsonTranslator.translate(JSON.stringify(replacedJsonPointersConfig.data), {
- vars: replacedJsonPointersConfig.l10n.vars,
- translations: replacedJsonPointersConfig.l10n.translations,
- data: {
- answers: questionnaire.answers
- }
- })
- );
- });
-
- if (
- onCompleteTasks.sendEmail.data.emailAddress === '' &&
- onCompleteTasks.sendSms.data.phoneNumber === ''
- ) {
- return false;
- }
-
- const notifyService = createNotifyService({logger});
- if (onCompleteTasks.sendEmail.data.emailAddress !== '') {
- notifyService.sendEmail(onCompleteTasks.sendEmail.data);
- return onCompleteTasks.sendEmail.data;
- }
- notifyService.sendSms(onCompleteTasks.sendSms.data);
- return onCompleteTasks.sendSms.data;
- } catch (err) {
- logger.error({err}, 'NOTIFICATION SENDING FAILED');
- return false;
- }
- }
-
async function getSubmissionResponseData(questionnaireId, isPostRequest = false) {
let submissionStatus = await getQuestionnaireSubmissionStatus(questionnaireId);
@@ -525,6 +477,31 @@ function createQuestionnaireService({
};
}
+ // TODO: Move this functionality to q-router
+ async function runOnCompleteActions(questionnaireDefinition) {
+ const questionnaire = createQuestionnaireHelper({
+ questionnaireDefinition
+ });
+ const permittedActions = questionnaire.getPermittedActions();
+ const actionResults = permittedActions.map(action => {
+ if (action.type === 'sendEmail') {
+ const notifyService = createNotifyService({logger});
+
+ return notifyService.sendEmail(action.data);
+ }
+
+ if (action.type === 'sendSms') {
+ const notifyService = createNotifyService({logger});
+
+ return notifyService.sendSms(action.data);
+ }
+
+ return Promise.reject(Error(`Action type "${action.type}" is not supported`));
+ });
+
+ return actionResults;
+ }
+
return Object.freeze({
createQuestionnaire,
createAnswers,
@@ -534,7 +511,7 @@ function createQuestionnaireService({
validateAllAnswers,
getAnswers,
getProgressEntries,
- sendConfirmationNotification,
+ runOnCompleteActions,
updateQuestionnaireSubmissionStatus
});
}
diff --git a/questionnaire/questionnaire/questionnaire.js b/questionnaire/questionnaire/questionnaire.js
index 80d9d4e9..cc6519dc 100644
--- a/questionnaire/questionnaire/questionnaire.js
+++ b/questionnaire/questionnaire/questionnaire.js
@@ -8,6 +8,8 @@ defaults.mutateObjectValues = require('./utils/mutateObjectValues');
defaults.getValueInterpolator = require('./utils/getValueInterpolator');
defaults.getValueContextualiser = require('./utils/getValueContextualiser');
defaults.deepClone = require('./utils/deepCloneJsonDerivedObject');
+defaults.getJsonExpressionEvaluator = require('./utils/getJsonExpressionEvaluator');
+defaults.qExpression = require('q-expressions');
function createQuestionnaire({
questionnaireDefinition,
@@ -17,7 +19,9 @@ function createQuestionnaire({
mutateObjectValues = defaults.mutateObjectValues,
getValueInterpolator = defaults.getValueInterpolator,
getValueContextualiser = defaults.getValueContextualiser,
- deepClone = defaults.deepClone
+ deepClone = defaults.deepClone,
+ getJsonExpressionEvaluator = defaults.getJsonExpressionEvaluator,
+ qExpression = defaults.qExpression
}) {
function getProgress() {
return questionnaireDefinition.progress || [];
@@ -35,6 +39,10 @@ function createQuestionnaire({
return allProgress;
}
+ function getRoles() {
+ return questionnaireDefinition?.attributes?.q__roles || {};
+ }
+
function getAnswers() {
return questionnaireDefinition.answers;
}
@@ -208,12 +216,18 @@ function createQuestionnaire({
const valueInterpolator = getValueInterpolator(allQuestionnaireAnswers);
if (sectionDefinition.l10n !== undefined) {
+ const jsonExpressionEvaluator = getJsonExpressionEvaluator({
+ ...allQuestionnaireAnswers,
+ attributes: {
+ q__roles: getRoles()
+ }
+ });
const valueContextualier = getValueContextualiser(
sectionDefinition,
allQuestionnaireAnswers
);
- orderedValueTransformers.push(valueContextualier);
+ orderedValueTransformers.push(jsonExpressionEvaluator, valueContextualier);
}
if (sectionDefinitionVars !== undefined && allowSummary === true) {
@@ -296,6 +310,47 @@ function createQuestionnaire({
return undefined;
}
+ function getPermittedActions() {
+ const actions = questionnaireDefinition?.meta?.onComplete?.actions;
+
+ if (actions) {
+ const allQuestionnaireAnswers = {answers: getAnswers()};
+ const permittedActions = actions.filter(action => {
+ if ('cond' in action) {
+ const isPermittedAction = qExpression.evaluate(
+ action.cond,
+ allQuestionnaireAnswers
+ );
+
+ return isPermittedAction;
+ }
+
+ return true;
+ });
+ const valueInterpolator = getValueInterpolator(allQuestionnaireAnswers);
+ const jsonExpressionEvaluator = getJsonExpressionEvaluator({
+ ...allQuestionnaireAnswers,
+ attributes: {
+ q__roles: getRoles()
+ }
+ });
+ const resolvedActions = permittedActions.map(permittedAction => {
+ if ('data' in permittedAction) {
+ mutateObjectValues(permittedAction.data, [
+ jsonExpressionEvaluator,
+ valueInterpolator
+ ]);
+ }
+
+ return permittedAction;
+ });
+
+ return resolvedActions;
+ }
+
+ return [];
+ }
+
return Object.freeze({
getTaxonomy,
getSection,
@@ -303,7 +358,8 @@ function createQuestionnaire({
getDataAttributes,
getNormalisedDetailsForAttribute,
getProgress, // TODO: remove this when declaration is handled correctly
- getAnswers // TODO: remove this when declaration is handled correctly
+ getAnswers, // TODO: remove this when declaration is handled correctly
+ getPermittedActions
});
}
diff --git a/questionnaire/questionnaire/questionnaire.test.js b/questionnaire/questionnaire/questionnaire.test.js
index 808ec5a1..fc5940f3 100644
--- a/questionnaire/questionnaire/questionnaire.test.js
+++ b/questionnaire/questionnaire/questionnaire.test.js
@@ -43,6 +43,62 @@ const questionnaireDefinition = {
}
}
},
+ attributes: {
+ q__roles: {
+ mainapplicant: {
+ schema: {
+ title: 'Main Applicant role',
+ type: 'boolean',
+ // prettier-ignore
+ const: ['or',
+ ['==', '$.answers.p-mainapplicant-parent.q-mainapplicant-parent', true],
+ ['==', '$.answers.p--has-legal-authority.q--has-legal-authority', true]
+ ]
+ }
+ },
+ rep: {
+ schema: {
+ title: 'Rep role',
+ type: 'boolean',
+ // prettier-ignore
+ const: ['==', '$.answers.p--has-legal-authority.q--has-legal-authority', false]
+ }
+ },
+ child: {
+ schema: {
+ title: 'Child applicant role',
+ type: 'boolean',
+ // prettier-ignore
+ const: ['==',
+ '$.answers.p-applicant-are-you-18-or-over.q-applicant-are-you-18-or-over',
+ false
+ ]
+ }
+ },
+ adult: {
+ schema: {
+ title: 'Adult applicant role',
+ type: 'boolean',
+ // prettier-ignore
+ const: ['==',
+ '$.answers.p-applicant-are-you-18-or-over.q-applicant-are-you-18-or-over',
+ true
+ ]
+ }
+ },
+ proxy: {
+ schema: {
+ title: 'A type of proxy for the applicant',
+ type: 'boolean',
+ // prettier-ignore
+ const: ['==',
+ '$.answers.p-applicant-who-are-you-applying-for.q-applicant-who-are-you-applying-for',
+ 'someone-else'
+ ]
+ }
+ }
+ }
+ },
sections: {
'p-applicant-date-of-birth': {
schema: {
@@ -219,6 +275,132 @@ const questionnaireDefinition = {
]
}
},
+ 'p-mainapplicant-name': {
+ l10n: {
+ vars: {
+ lng: 'en',
+ ns: 'p-mainapplicant-name'
+ },
+ translations: [
+ {
+ language: 'en',
+ namespace: 'p-mainapplicant-name',
+ resources: {
+ title: {
+ mainapplicant: 'Enter your name',
+ rep: {
+ child:
+ 'Enter the name of the person with parental responsibility for the victim',
+ adult:
+ 'Enter the name of the person with legal authority for the victim'
+ }
+ },
+ 'summary-title': {
+ mainapplicant: 'Your name',
+ rep: {
+ child:
+ 'Name of the person with parental responsibility for the victim',
+ adult: 'Name of the person with legal authority for the victim'
+ }
+ }
+ }
+ }
+ ]
+ },
+ schema: {
+ $schema: 'http://json-schema.org/draft-07/schema#',
+ type: 'object',
+ allOf: [
+ {
+ // prettier-ignore
+ title: ['|l10nt',
+ ['|role.any', 'mainapplicant'], 'title.mainapplicant',
+ ['|role.all', 'rep', 'adult'], 'title.rep.adult',
+ ['|role.all', 'rep', 'child'], 'title.rep.child',
+ ],
+ meta: {
+ compositeId: 'mainapplicant-name',
+ classifications: {
+ theme: 'mainapplicant-details'
+ },
+ summary: {
+ // prettier-ignore
+ title: ['|l10nt',
+ ['|role.any', 'mainapplicant'], 'summary-title.mainapplicant',
+ ['|role.all', 'rep', 'adult'], 'summary-title.rep.adult',
+ ['|role.all', 'rep', 'child'], 'summary-title.rep.child'
+ ]
+ }
+ },
+ required: [
+ 'q-mainapplicant-title',
+ 'q-mainapplicant-first-name',
+ 'q-mainapplicant-last-name'
+ ],
+ propertyNames: {
+ enum: [
+ 'q-mainapplicant-title',
+ 'q-mainapplicant-first-name',
+ 'q-mainapplicant-last-name'
+ ]
+ },
+ allOf: [
+ {
+ properties: {
+ 'q-mainapplicant-title': {
+ title: 'Title',
+ type: 'string',
+ maxLength: 6,
+ errorMessage: {
+ maxLength: 'Title must be 6 characters or less'
+ },
+ meta: {
+ classifications: {
+ theme: 'mainapplicant-details'
+ }
+ }
+ }
+ }
+ },
+ {
+ properties: {
+ 'q-mainapplicant-first-name': {
+ title: 'First name',
+ type: 'string',
+ maxLength: 70,
+ errorMessage: {
+ maxLength: 'First name must be 70 characters or less'
+ },
+ meta: {
+ classifications: {
+ theme: 'mainapplicant-details'
+ }
+ }
+ }
+ }
+ },
+ {
+ properties: {
+ 'q-mainapplicant-last-name': {
+ title: 'Last name',
+ type: 'string',
+ maxLength: 70,
+ errorMessage: {
+ maxLength: 'Last name must be 70 characters or less'
+ },
+ meta: {
+ classifications: {
+ theme: 'mainapplicant-details'
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
'p-applicant-british-citizen-or-eu-national': {
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
@@ -372,6 +554,12 @@ const questionnaireDefinition = {
'p-applicant-theme-not-found': {
'q-applicant-theme-not-found': 'blue'
},
+ 'p--has-legal-authority': {
+ 'q--has-legal-authority': false
+ },
+ 'p-applicant-are-you-18-or-over': {
+ 'q-applicant-are-you-18-or-over': false
+ },
'p--check-your-answers': {}
}
};
@@ -519,6 +707,98 @@ describe('Questionnaire', () => {
]
});
});
+
+ it('should return a personalised section', () => {
+ const questionnaire = createQuestionnaire({questionnaireDefinition});
+ const section = questionnaire.getSection('p-mainapplicant-name');
+ const sectionSchema = section.getSchema();
+
+ expect(sectionSchema).toEqual({
+ $schema: 'http://json-schema.org/draft-07/schema#',
+ type: 'object',
+ allOf: [
+ {
+ title:
+ 'Enter the name of the person with parental responsibility for the victim',
+ meta: {
+ compositeId: 'mainapplicant-name',
+ classifications: {
+ theme: 'mainapplicant-details'
+ },
+ summary: {
+ title:
+ 'Name of the person with parental responsibility for the victim'
+ }
+ },
+ required: [
+ 'q-mainapplicant-title',
+ 'q-mainapplicant-first-name',
+ 'q-mainapplicant-last-name'
+ ],
+ propertyNames: {
+ enum: [
+ 'q-mainapplicant-title',
+ 'q-mainapplicant-first-name',
+ 'q-mainapplicant-last-name'
+ ]
+ },
+ allOf: [
+ {
+ properties: {
+ 'q-mainapplicant-title': {
+ title: 'Title',
+ type: 'string',
+ maxLength: 6,
+ errorMessage: {
+ maxLength: 'Title must be 6 characters or less'
+ },
+ meta: {
+ classifications: {
+ theme: 'mainapplicant-details'
+ }
+ }
+ }
+ }
+ },
+ {
+ properties: {
+ 'q-mainapplicant-first-name': {
+ title: 'First name',
+ type: 'string',
+ maxLength: 70,
+ errorMessage: {
+ maxLength: 'First name must be 70 characters or less'
+ },
+ meta: {
+ classifications: {
+ theme: 'mainapplicant-details'
+ }
+ }
+ }
+ }
+ },
+ {
+ properties: {
+ 'q-mainapplicant-last-name': {
+ title: 'Last name',
+ type: 'string',
+ maxLength: 70,
+ errorMessage: {
+ maxLength: 'Last name must be 70 characters or less'
+ },
+ meta: {
+ classifications: {
+ theme: 'mainapplicant-details'
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ });
+ });
});
describe('Given a section definition requiring an answer summary', () => {
@@ -665,4 +945,138 @@ describe('Questionnaire', () => {
});
});
});
+
+ describe('Given an "onComplete" actions definition', () => {
+ it('should return all actions that pass their condition', () => {
+ const questionnaire = createQuestionnaire({
+ questionnaireDefinition: {
+ meta: {
+ onComplete: {
+ actions: [
+ {
+ cond: ['==', 1, 1],
+ type: 'actionA'
+ },
+ {
+ type: 'actionD'
+ },
+ {
+ cond: ['==', 3, 4],
+ type: 'actionC'
+ },
+ {
+ cond: ['==', 2, 2],
+ type: 'actionB'
+ }
+ ]
+ }
+ }
+ }
+ });
+ const actions = questionnaire.getPermittedActions();
+ const actionTypes = actions.map(action => action.type);
+
+ expect(actionTypes.length).toEqual(3);
+ expect(actionTypes).toEqual(['actionA', 'actionD', 'actionB']);
+ });
+
+ it('should allow action conditions to reference context', () => {
+ const questionnaire = createQuestionnaire({
+ questionnaireDefinition: {
+ meta: {
+ onComplete: {
+ actions: [
+ {
+ cond: ['==', '$.answers.p-page.q-question', 'foo'],
+ type: 'actionA'
+ },
+ {
+ cond: ['==', '$.answers.p-page.q-question', 'bar'],
+ type: 'actionB'
+ },
+ {
+ type: 'actionD'
+ }
+ ]
+ }
+ },
+ answers: {
+ 'p-page': {
+ 'q-question': 'bar'
+ }
+ }
+ }
+ });
+
+ const actions = questionnaire.getPermittedActions();
+ const actionTypes = actions.map(action => action.type);
+
+ expect(actionTypes.length).toEqual(2);
+ expect(actionTypes).toEqual(['actionB', 'actionD']);
+ });
+
+ it('should allow action data to reference context', () => {
+ const questionnaire = createQuestionnaire({
+ questionnaireDefinition: {
+ meta: {
+ onComplete: {
+ actions: [
+ {
+ cond: ['==', 1, 1],
+ type: 'actionA',
+ data: {
+ foo: '||/answers/p-page/q-question||'
+ }
+ }
+ ]
+ }
+ },
+ answers: {
+ 'p-page': {
+ 'q-question': 'bar'
+ }
+ }
+ }
+ });
+ const actions = questionnaire.getPermittedActions();
+
+ expect(actions[0].data).toEqual({
+ foo: 'bar'
+ });
+ });
+
+ it('should allow action data to contain JSON expressions', () => {
+ const questionnaire = createQuestionnaire({
+ questionnaireDefinition: {
+ meta: {
+ onComplete: {
+ actions: [
+ {
+ cond: ['==', 1, 1],
+ type: 'actionA',
+ data: {
+ // prettier-ignore
+ bar: ['|cond',
+ ['==', '$.answers.p-page.q-question', 'foo'], 'fooValue',
+ ['==', '$.answers.p-page.q-question', 'bar'], 'barValue'
+ ]
+ }
+ }
+ ]
+ }
+ },
+ answers: {
+ 'p-page': {
+ 'q-question': 'bar'
+ }
+ }
+ }
+ });
+ const actions = questionnaire.getPermittedActions();
+
+ expect(actions[0].data).toEqual({
+ bar: 'barValue'
+ });
+ });
+ });
});
diff --git a/questionnaire/questionnaire/utils/getJsonExpressionEvaluator/index.js b/questionnaire/questionnaire/utils/getJsonExpressionEvaluator/index.js
new file mode 100644
index 00000000..432a9ff7
--- /dev/null
+++ b/questionnaire/questionnaire/utils/getJsonExpressionEvaluator/index.js
@@ -0,0 +1,15 @@
+'use strict';
+
+const qExpression = require('q-expressions');
+
+function evaluateJsonExpression(data) {
+ return (key, value) => {
+ if (qExpression.isJsonExpression(value)) {
+ return qExpression.evaluate(value, data);
+ }
+
+ return value;
+ };
+}
+
+module.exports = evaluateJsonExpression;
diff --git a/questionnaire/questionnaire/utils/mutateObjectValues/index.js b/questionnaire/questionnaire/utils/mutateObjectValues/index.js
index d4e358e6..151a56fd 100644
--- a/questionnaire/questionnaire/utils/mutateObjectValues/index.js
+++ b/questionnaire/questionnaire/utils/mutateObjectValues/index.js
@@ -1,7 +1,15 @@
'use strict';
+const qExpression = require('q-expressions');
+
+const {isJsonExpression} = qExpression;
+
+function isPlainObjectOrArray(value) {
+ return value && typeof value === 'object' && isJsonExpression(value) === false;
+}
+
function mutateObjectValues(value, valueTransformers, key) {
- if (value && typeof value === 'object') {
+ if (isPlainObjectOrArray(value)) {
Object.entries(value).forEach(([k, v]) => {
value[k] = mutateObjectValues(v, valueTransformers, k);
});
diff --git a/questionnaire/routes.js b/questionnaire/routes.js
index 829ad3d6..8395cf8f 100644
--- a/questionnaire/routes.js
+++ b/questionnaire/routes.js
@@ -74,7 +74,13 @@ router
req.params.questionnaireId,
'COMPLETED'
);
- questionnaireService.sendConfirmationNotification(req.params.questionnaireId);
+
+ const questionnireDefinition = await questionnaireService.getQuestionnaire(
+ req.params.questionnaireId
+ );
+
+ // Currently, fire and forget. No await required
+ questionnaireService.runOnCompleteActions(questionnireDefinition);
res.status(201).json(response);
} catch (err) {
diff --git a/questionnaire/runOnCompleteActions.test.js b/questionnaire/runOnCompleteActions.test.js
new file mode 100644
index 00000000..efee125c
--- /dev/null
+++ b/questionnaire/runOnCompleteActions.test.js
@@ -0,0 +1,162 @@
+'use strict';
+
+jest.doMock('../services/notify', () => {
+ const notifyServiceMock = {
+ sendEmail: jest.fn().mockResolvedValue({
+ some: 'email response'
+ }),
+ sendSms: jest.fn().mockResolvedValue({
+ some: 'sms response'
+ })
+ };
+
+ return () => notifyServiceMock;
+});
+
+const mockedNotifyService = require('../services/notify')();
+const createQuestionnaireService = require('./questionnaire-service');
+
+describe('runOnCompleteActions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('Given a send email action', () => {
+ it('should call the send email function with defined data', async () => {
+ const questionnaireService = createQuestionnaireService();
+ const actionData = {some: 'action data 47c7fc59-e657-4d10-b57e-2b3a59bd9bdf'};
+ const actionResults = await Promise.allSettled(
+ await questionnaireService.runOnCompleteActions({
+ meta: {
+ onComplete: {
+ actions: [
+ {
+ type: 'sendEmail',
+ data: actionData
+ }
+ ]
+ }
+ }
+ })
+ );
+
+ expect(actionResults).toEqual([{status: 'fulfilled', value: {some: 'email response'}}]);
+ expect(mockedNotifyService.sendEmail).toHaveBeenCalledTimes(1);
+ expect(mockedNotifyService.sendEmail).toHaveBeenCalledWith(actionData);
+ });
+ });
+
+ describe('Given a send sms action', () => {
+ it('should call the send sms function with defined data', async () => {
+ const questionnaireService = createQuestionnaireService();
+ const actionData = {some: 'action data 790edce0-4f90-4d3d-8fe5-52889c2caa00'};
+ const actionResults = await Promise.allSettled(
+ await questionnaireService.runOnCompleteActions({
+ meta: {
+ onComplete: {
+ actions: [
+ {
+ type: 'sendSms',
+ data: actionData
+ }
+ ]
+ }
+ }
+ })
+ );
+
+ expect(actionResults).toEqual([{status: 'fulfilled', value: {some: 'sms response'}}]);
+ expect(mockedNotifyService.sendSms).toHaveBeenCalledTimes(1);
+ expect(mockedNotifyService.sendSms).toHaveBeenCalledWith(actionData);
+ });
+ });
+
+ describe('Given multiple actions', () => {
+ it('should run each action in parallel', async () => {
+ const questionnaireService = createQuestionnaireService();
+ const smsActionData = {some: 'action data ff10551c-9928-410a-a7be-5ba21297a132'};
+ const emailActionData1 = {some: 'action data 0bdb9b24-5d1f-4c46-b706-bd7edcf3c87b'};
+ const emailActionData2 = {some: 'action data 83a376f2-6329-474a-a055-7510c0b7befd'};
+ const actionResults = await Promise.allSettled(
+ await questionnaireService.runOnCompleteActions({
+ meta: {
+ onComplete: {
+ actions: [
+ {
+ type: 'sendSms',
+ data: smsActionData
+ },
+ {
+ type: 'sendEmail',
+ data: emailActionData1
+ },
+ {
+ type: 'sendEmail',
+ data: emailActionData2
+ }
+ ]
+ }
+ }
+ })
+ );
+
+ expect(actionResults).toEqual([
+ {status: 'fulfilled', value: {some: 'sms response'}},
+ {status: 'fulfilled', value: {some: 'email response'}},
+ {status: 'fulfilled', value: {some: 'email response'}}
+ ]);
+ expect(mockedNotifyService.sendSms).toHaveBeenCalledTimes(1);
+ expect(mockedNotifyService.sendSms).toHaveBeenCalledWith(smsActionData);
+ expect(mockedNotifyService.sendEmail).toHaveBeenCalledTimes(2);
+ expect(mockedNotifyService.sendEmail).toHaveBeenNthCalledWith(1, emailActionData1);
+ expect(mockedNotifyService.sendEmail).toHaveBeenNthCalledWith(2, emailActionData2);
+ });
+ });
+
+ describe('Given multiple actions where one or more fail', () => {
+ it('should return rejected promises', async () => {
+ const questionnaireService = createQuestionnaireService();
+ const smsActionData = {some: 'action data 83787b54-8d41-4516-af1b-75e1ae2feec7'};
+ const emailActionData1 = {some: 'action data 90aa74f3-dcd8-4e10-becb-fe5dbd2c67c6'};
+ const emailActionData2 = {some: 'action data 4f8267d9-08fb-483a-940c-2431f2c4d143'};
+
+ mockedNotifyService.sendSms.mockRejectedValue({
+ some: 'sms error'
+ });
+
+ const actionResults = await Promise.allSettled(
+ await questionnaireService.runOnCompleteActions({
+ meta: {
+ onComplete: {
+ actions: [
+ {
+ type: 'sendSms',
+ data: smsActionData
+ },
+ {
+ type: 'sendEmail',
+ data: emailActionData1
+ },
+ {
+ type: 'sendEmail',
+ data: emailActionData2
+ }
+ ]
+ }
+ }
+ })
+ );
+
+ expect(actionResults).toEqual([
+ {status: 'rejected', reason: {some: 'sms error'}},
+ {status: 'fulfilled', value: {some: 'email response'}},
+ {status: 'fulfilled', value: {some: 'email response'}}
+ ]);
+ expect(mockedNotifyService.sendSms).toHaveBeenCalledTimes(1);
+ expect(mockedNotifyService.sendSms).toHaveBeenCalledWith(smsActionData);
+ expect(mockedNotifyService.sendEmail).toHaveBeenCalledTimes(2);
+ expect(mockedNotifyService.sendEmail).toHaveBeenNthCalledWith(1, emailActionData1);
+ expect(mockedNotifyService.sendEmail).toHaveBeenNthCalledWith(2, emailActionData2);
+ });
+ });
+});
diff --git a/services/notify/index.external.integration.test.js b/services/notify/index.external.integration.test.js
index 8818a7e9..460c2a1a 100644
--- a/services/notify/index.external.integration.test.js
+++ b/services/notify/index.external.integration.test.js
@@ -142,4 +142,29 @@ describe('Notify service', () => {
expect(smsSendRequest).toEqual(undefined);
});
});
+
+ describe('Given an issue with the email send request', () => {
+ it('should log an api error', async () => {
+ const mockLogger = {error: jest.fn()};
+ const invalidUrl = 'https://220499a5-cc54-47a9-96c3-6c136df0db83.gov.uk/does-not-exist';
+ const notifyService = createNotifyService({
+ logger: mockLogger,
+ notifyClient: mockNotifyClient,
+ url: invalidUrl // this gets passed through to the internal message bus caller
+ });
+
+ await notifyService.sendEmail({
+ templateId: '3c847bb8-957a-4bba-9fad-090657bb5c71',
+ email: 'foo@fbd962de-628b-4087-b452-410a36d00fef.gov.uk',
+ personalisation: {
+ caseReference: DUMMY_CASE_REFERENCE
+ }
+ });
+
+ expect(mockLogger.error).toHaveBeenCalledWith(
+ {code: 'ENOTFOUND'},
+ 'EMAIL SEND FAILURE'
+ );
+ });
+ });
});
diff --git a/services/notify/index.js b/services/notify/index.js
index 92f385c8..4ac3d6fc 100644
--- a/services/notify/index.js
+++ b/services/notify/index.js
@@ -57,15 +57,20 @@ function createNotifyService(spec) {
}
async function sendEmail(options) {
- const messageBus = createMessageBusCaller(spec);
- return messageBus.post('NotificationQueue', {
- templateId: options.templateId,
- emailAddress: options.emailAddress,
- personalisation: {
- caseReference: options.personalisation.caseReference
- },
- reference: null
- });
+ try {
+ const messageBus = createMessageBusCaller(spec);
+
+ await messageBus.post('NotificationQueue', {
+ templateId: options.templateId,
+ emailAddress: options.emailAddress,
+ personalisation: {
+ caseReference: options.personalisation.caseReference
+ },
+ reference: null
+ });
+ } catch (err) {
+ logger.error({code: err.code}, 'EMAIL SEND FAILURE');
+ }
}
return Object.freeze({