diff --git a/database/firestore.rules b/database/firestore.rules
index 19febe9eb..637763a5b 100644
--- a/database/firestore.rules
+++ b/database/firestore.rules
@@ -70,6 +70,9 @@ service cloud.firestore {
match /reports/{reportId} {
allow read: if userIsAuthenticated() && userIsJAC() && hasPermission('e1');
}
+ match /reports/sccSummary { // specific rule for sccSummary report
+ allow write: if userIsAuthenticated() && userIsJAC() && hasPermission('e3');
+ }
match /tasks/{taskId} {
allow create, update: if userIsAuthenticated() && userIsJAC();
allow read: if userIsAuthenticated() && userIsJAC();
diff --git a/functions/actions/exercises/sccSummaryReport.js b/functions/actions/exercises/sccSummaryReport.js
new file mode 100644
index 000000000..f1279b8d5
--- /dev/null
+++ b/functions/actions/exercises/sccSummaryReport.js
@@ -0,0 +1,237 @@
+import { getDocument, formatDate } from '../../shared/helpers.js';
+import htmlWriter from '../../shared/htmlWriter.js';
+import config from '../../shared/config.js';
+import initDrive from '../../shared/google-drive.js';
+import initExerciseHelper from '../../shared/exerciseHelper.js';
+
+const drive = initDrive();
+
+export default (firebase, db) => {
+ const { SELECTION_CATEGORIES, applicationCounts, shortlistingMethods, formatSelectionDays } = initExerciseHelper(config);
+ const { APPLICATION_STATUS } = config;
+
+ return {
+ generateSccSummaryReport,
+ exportSccSummaryReport,
+ };
+
+ async function generateSccSummaryReport(exerciseId) {
+ const exercise = await getDocument(db.collection('exercises').doc(exerciseId));
+ const sccSummaryReport = await db.collection('exercises').doc(exerciseId).collection('reports').doc('sccSummary').get();
+ const reportData = sccSummaryReport.exists ? sccSummaryReport.data() : {};
+
+ // set the report-specific fields from the report document
+ const numberOfCandidatesProposedForRecommendation = reportData.numberOfCandidatesProposedForRecommendation || '';
+ const shortlistingDates = reportData.shortlistingDates || '';
+ const statutoryConsultees = reportData.statutoryConsultees || '';
+ const vr = reportData.vr || '';
+ const dateS94ListCreated = reportData.dateS94ListCreated || '';
+ const candidatesRemainingOnS94List = reportData.candidatesRemainingOnS94List || '';
+ const vacanciesByJurisdictionChamber = reportData.vacanciesByJurisdictionChamber || '';
+ const characterChecksUndertaken = reportData.characterChecksUndertaken || '';
+ const numberOfACandidates = reportData.numberOfACandidates || '';
+ const characterIssues = reportData.characterIssues || '';
+ const mattersRequiringADecision = reportData.mattersRequiringADecision || '';
+ const previouslyDeclaredWithinGuidance = reportData.previouslyDeclaredWithinGuidance || '';
+ const highScoringDCandidates = reportData.highScoringDCandidates || '';
+
+ // set the report-specific fields from the exercise document
+ let numberOfVacancies = [];
+ if (exercise.immediateStart > 0) {
+ numberOfVacancies.push(`${exercise.immediateStart} Immediate start (S87)`);
+ }
+ if (exercise.futureStart > 0) {
+ numberOfVacancies.push(`${exercise.futureStart} Future start (S94)`);
+ }
+
+ const locationDetails = exercise.location;
+ const launch = formatDate(exercise.applicationOpenDate);
+ const closed = formatDate(exercise.applicationCloseDate);
+
+ const applicationCountValues = applicationCounts(exercise);
+ const numberOfApplications = applicationCountValues.applied || 0;
+ const numberOfWithdrawals = applicationCountValues.withdrawn || 0;
+
+ let numberOfRemovedOnEligibilityOrASC = 0;
+ let numberOfShortlisted = 0;
+
+ if (exercise._applicationRecords.status) {
+ numberOfRemovedOnEligibilityOrASC =
+ (exercise._applicationRecords.status[APPLICATION_STATUS.REJECTED_INELIGIBLE_STATUTORY] || 0) +
+ (exercise._applicationRecords.status[APPLICATION_STATUS.REJECTED_INELIGIBLE_ADDITIONAL] || 0);
+ numberOfShortlisted = exercise._applicationRecords.status[APPLICATION_STATUS.SHORTLISTING_PASSED] || 0;
+ }
+
+ const shortlistingMethod = shortlistingMethods(exercise);
+ const selectionCategories = exercise.selectionCategories || [];
+ const selectionDayTools = Object.values(SELECTION_CATEGORIES).filter((c) => selectionCategories.includes(c.value)).map((c) => c.description);
+ const datesOfSelectionDays = exercise.selectionDays || [];
+
+ // construct the report document
+ const report = {
+ numberOfVacancies: numberOfVacancies.join(', '),
+ locationDetails,
+ launch,
+ closed,
+ numberOfApplications,
+ numberOfWithdrawals,
+ numberOfRemovedOnEligibilityOrASC,
+ numberOfShortlisted,
+ selectionDayTools: selectionDayTools.join(', '),
+ datesOfSelectionDays,
+ numberOfACandidates,
+ characterChecksUndertaken,
+ characterIssues,
+ mattersRequiringADecision,
+ previouslyDeclaredWithinGuidance,
+ highScoringDCandidates,
+ numberOfCandidatesProposedForRecommendation,
+ vacanciesByJurisdictionChamber,
+ vr,
+ shortlistingDates,
+ statutoryConsultees: statutoryConsultees.length > 0 ? statutoryConsultees : 'Insert name(s) or Consultation waived',
+ dateS94ListCreated,
+ candidatesRemainingOnS94List,
+ shortlistingMethod: shortlistingMethod.join(', '),
+ };
+
+ // store the report document in the database
+ await db.collection('exercises').doc(exerciseId).collection('reports').doc('sccSummary').set(report, { merge: true });
+
+ // return the report in the HTTP response
+ return report;
+ }
+
+ /**
+ * exportSccSummaryReport
+ * Generates an export of the SCC Summary report for the selected exercise
+ * @param {*} `exerciseId` (required) ID of exercise to include in the export
+ */
+ async function exportSccSummaryReport(exerciseId) {
+
+ const exercise = await getDocument(db.collection('exercises').doc(exerciseId));
+ const sccSummaryReport = await generateSccSummaryReport(exerciseId);
+
+ // get drive service
+ await drive.login();
+
+ // get settings and apply them
+ const settings = await getDocument(db.collection('settings').doc('services'));
+ drive.setDriveId(settings.google.driveId);
+
+ // generate a filename for the document we are going to create ex. JAC00787_SCC Summary
+ const now = new Date();
+ // const timestamp = new Date(now.getTime() - (now.getTimezoneOffset() * 60000)).toISOString();
+ const filename = `${exercise.referenceNumber}_SCC Summary` ;
+
+ // make sure a destination folder exists to create the file in
+ const folderName = 'SCC Summary Export';
+ const folders = await drive.listFolders();
+ let folderId = 0;
+ folders.forEach((v, i) => {
+ if (v.name === folderName) {
+ folderId = v.id;
+ }
+ });
+ if (folderId === 0) { // folder doesn't exist so create it
+ folderId = await drive.createFolder(folderName);
+ }
+
+ // Create SCC Summary document
+ const fileId = await drive.createFile(filename, {
+ folderId: folderId,
+ sourceType: drive.MIME_TYPE.HTML,
+ sourceContent: getHtmlSccSummaryReport(sccSummaryReport),
+ destinationType: drive.MIME_TYPE.DOCUMENT,
+ });
+
+ if (fileId) {
+ return await drive.exportFile(fileId, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
+ }
+
+ return false;
+ }
+
+ function getHtmlSccSummaryReport(sccSummaryReport) {
+ const {
+ numberOfVacancies,
+ locationDetails,
+ launch,
+ closed,
+ numberOfApplications,
+ numberOfWithdrawals,
+ numberOfRemovedOnEligibilityOrASC,
+ numberOfShortlisted,
+ selectionDayTools,
+ datesOfSelectionDays,
+ numberOfACandidates,
+ characterChecksUndertaken,
+ characterIssues,
+ mattersRequiringADecision,
+ previouslyDeclaredWithinGuidance,
+ highScoringDCandidates,
+ numberOfCandidatesProposedForRecommendation,
+ vacanciesByJurisdictionChamber,
+ vr,
+ shortlistingDates,
+ statutoryConsultees,
+ dateS94ListCreated,
+ candidatesRemainingOnS94List,
+ shortlistingMethod,
+ } = sccSummaryReport;
+
+ const rows = [
+ { header: 'Number of vacancies', content: numberOfVacancies },
+ { header: 'Number of candidates proposed for Recommendation', content: numberOfCandidatesProposedForRecommendation },
+ { header: 'Vacancies by jurisdiction/chamber', content: vacanciesByJurisdictionChamber },
+ { header: 'Location details', content: locationDetails },
+ { header: 'VR (includes details of SPTW)', content: `Annex ${vr}` },
+ { header: '', content: '' },
+ { header: 'Launch', content: formatDate(launch, 'DD/MM/YYYY') },
+ { header: 'Closed', content: formatDate(closed, 'DD/MM/YYYY') },
+ { header: 'Number of applications', content: numberOfApplications },
+ { header: 'Number of withdrawals', content: numberOfWithdrawals },
+ { header: 'Number removed on eligibility/ASC', content: numberOfRemovedOnEligibilityOrASC },
+ { header: '', content: '' },
+ { header: 'Number shortlisted', content: numberOfShortlisted },
+ { header: 'Method of shortlisting', content: shortlistingMethod },
+ { header: 'Dates of shortlisting', content: shortlistingDates },
+ { header: '', content: '' },
+ { header: 'Statutory consultee(s)', content: statutoryConsultees },
+ { header: '', content: '' },
+ { header: 'Selection day tools', content: selectionDayTools },
+ { header: 'Number of A-C candidates', content: numberOfACandidates },
+ { header: 'Dates of selection days', content: formatSelectionDays({ selectionDays: datesOfSelectionDays}).join('
') },
+ { header: '', content: '' },
+ { header: 'Date s.94 list Created', content: dateS94ListCreated },
+ { header: 'Candidates Remaining on s.94 list', content: candidatesRemainingOnS94List },
+ { header: '', content: '' },
+ { header: 'Character Checks undertaken', content: characterChecksUndertaken },
+ { header: 'Character issues', content: characterIssues },
+ { header: 'Matters requiring a decision', content: `Annex ${mattersRequiringADecision}` },
+ { header: 'Previously declared, within guidance', content: `Annex ${previouslyDeclaredWithinGuidance}` },
+ { header: 'High scoring D candidates', content: `Annex ${highScoringDCandidates}` },
+ ];
+
+ let writer = new htmlWriter();
+ writer.addHeading('SCC Summary', 'center');
+ writer.addRaw(`
+
+ `
+ );
+ for (const row of rows) {
+ writer.addRaw(
+ `
+ ${row.header} |
+ ${row.content} |
+
`
+ );
+ }
+ writer.addRaw(
+ `
+
`);
+
+ return writer.toString();
+ }
+};
+
diff --git a/functions/callableFunctions/exportSccSummaryReport.js b/functions/callableFunctions/exportSccSummaryReport.js
new file mode 100644
index 000000000..90c2bf144
--- /dev/null
+++ b/functions/callableFunctions/exportSccSummaryReport.js
@@ -0,0 +1,50 @@
+import functions from 'firebase-functions';
+import { firebase, db, auth } from '../shared/admin.js';
+import initExportSccSummaryReport from '../actions/exercises/sccSummaryReport.js';
+import { getDocument } from '../shared/helpers.js';
+import initLogEvent from '../actions/logs/logEvent.js';
+import initServiceSettings from '../shared/serviceSettings.js';
+import { PERMISSIONS, hasPermissions } from '../shared/permissions.js';
+
+const { exportSccSummaryReport } = initExportSccSummaryReport(firebase, db);
+const { logEvent } = initLogEvent(firebase, db, auth);
+const { checkFunctionEnabled } = initServiceSettings(db);
+
+export default functions.region('europe-west2').https.onCall(async (data, context) => {
+ await checkFunctionEnabled();
+
+ if (!context.auth) {
+ throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated.');
+ }
+
+ hasPermissions(context.auth.token.rp, [
+ PERMISSIONS.exercises.permissions.canReadExercises.value,
+ PERMISSIONS.applicationRecords.permissions.canReadApplicationRecords.value,
+ PERMISSIONS.applications.permissions.canReadApplications.value,
+ ]);
+
+ if (!(typeof data.exerciseId === 'string') || data.exerciseId.length === 0) {
+ throw new functions.https.HttpsError('invalid-argument', 'Please specify an "exerciseId"');
+ }
+
+ if (!(typeof data.format === 'string') || data.format.length === 0) {
+ throw new functions.https.HttpsError('invalid-argument', 'Please specify a data format (excel or googledoc)');
+ }
+
+ const exercise = await getDocument(db.collection('exercises').doc(data.exerciseId));
+
+ if (!exercise) {
+ throw new functions.https.HttpsError('not-found', 'Excercise not found');
+ }
+
+ let details = {
+ exerciseId: exercise.id,
+ exerciseRef: exercise.referenceNumber,
+ };
+ let user = {
+ id: context.auth.token.user_id,
+ name: context.auth.token.name,
+ };
+ await logEvent('info', 'SCC Summary report exported (to ' + data.format + ')', details, user);
+ return await exportSccSummaryReport(data.exerciseId, data.format, (data.status || null));
+});
diff --git a/functions/callableFunctions/generateSccSummaryReport.js b/functions/callableFunctions/generateSccSummaryReport.js
new file mode 100644
index 000000000..1c7bb51a2
--- /dev/null
+++ b/functions/callableFunctions/generateSccSummaryReport.js
@@ -0,0 +1,56 @@
+import functions from 'firebase-functions';
+import { firebase, db, auth } from '../shared/admin.js';
+import initGenerateSccSummaryReport from '../actions/exercises/sccSummaryReport.js';
+import { getDocument } from '../shared/helpers.js';
+import initLogEvent from '../actions/logs/logEvent.js';
+import initServiceSettings from '../shared/serviceSettings.js';
+import { PERMISSIONS, hasPermissions } from '../shared/permissions.js';
+
+const { generateSccSummaryReport } = initGenerateSccSummaryReport(firebase, db);
+const { logEvent } = initLogEvent(firebase, db, auth);
+const { checkFunctionEnabled } = initServiceSettings(db);
+
+const runtimeOptions = {
+ timeoutSeconds: 120,
+ memory: '512MB',
+};
+
+export default functions.runWith(runtimeOptions).region('europe-west2').https.onCall(async (data, context) => {
+ await checkFunctionEnabled();
+
+ // authenticate the request
+ if (!context.auth) {
+ throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated.');
+ }
+
+ hasPermissions(context.auth.token.rp, [
+ PERMISSIONS.exercises.permissions.canReadExercises.value,
+ PERMISSIONS.applications.permissions.canReadApplications.value,
+ PERMISSIONS.applicationRecords.permissions.canReadApplicationRecords.value,
+ ]);
+
+ // validate input parameters
+ if (!(typeof data.exerciseId === 'string') || data.exerciseId.length === 0) {
+ throw new functions.https.HttpsError('invalid-argument', 'Please specify an "exerciseId"');
+ }
+
+ // generate the report
+ const result = await generateSccSummaryReport(data.exerciseId);
+
+ // log an event
+ const exercise = await getDocument(db.collection('exercises').doc(data.exerciseId));
+ let details = {
+ exerciseId: exercise.id,
+ exerciseRef: exercise.referenceNumber,
+ };
+ let user = {
+ id: context.auth.token.user_id,
+ name: context.auth.token.name,
+ };
+ await logEvent('info', 'SCC Summary report generated', details, user);
+
+ // return the report to the caller
+ return {
+ result: result,
+ };
+});
diff --git a/functions/index.js b/functions/index.js
index 09c9dfa2d..6ee3f1a5c 100644
--- a/functions/index.js
+++ b/functions/index.js
@@ -97,6 +97,8 @@ import getLatestReleases from './callableFunctions/getLatestReleases.js';
import verifyFileChecksum from './callableFunctions/verifyFileChecksum.js';
import sendSmsVerificationCode from './callableFunctions/sendSmsVerificationCode.js';
import verifySmsVerificationCode from './callableFunctions/verifySmsVerificationCode.js';
+import generateSccSummaryReport from './callableFunctions/generateSccSummaryReport.js';
+import exportSccSummaryReport from './callableFunctions/exportSccSummaryReport.js';
import getMultipleApplicationData from './callableFunctions/getMultipleApplicationData.js';
// Callable - QTs v2
@@ -208,6 +210,8 @@ export {
sendSmsVerificationCode,
verifySmsVerificationCode,
getMultipleApplicationData,
+ generateSccSummaryReport,
+ exportSccSummaryReport,
// Callable - QTs v2
listQualifyingTests,
diff --git a/functions/shared/exerciseHelper.js b/functions/shared/exerciseHelper.js
index c9d92876e..daf3115ae 100644
--- a/functions/shared/exerciseHelper.js
+++ b/functions/shared/exerciseHelper.js
@@ -1,9 +1,45 @@
+import { formatDate } from './helpers.js';
+import lookup from './converters/lookup.js';
+import _ from 'lodash';
export default (config) => {
const EXERCISE_STAGE = config.EXERCISE_STAGE;
+
+ const SELECTION_CATEGORIES = {
+ LEADERSHIP: {
+ value: 'leadership',
+ label: 'Leadership',
+ description: 'Strategic Leadership Questions',
+ },
+ ROLEPLAY: {
+ value: 'roleplay',
+ label: 'Roleplay',
+ description: 'Role Play',
+ },
+ SITUATIONAL: {
+ value: 'situational',
+ label: 'Situational',
+ description: 'Situational Questions',
+ },
+ INTERVIEW: {
+ value: 'interview',
+ label: 'Interview',
+ description: 'Interview',
+ },
+ OVERALL: {
+ value: 'overall',
+ label: 'Overall',
+ description: 'Overall',
+ },
+ };
+
return {
+ SELECTION_CATEGORIES,
availableStages,
isStagedExercise,
canApplyFullApplicationSubmitted,
+ applicationCounts,
+ shortlistingMethods,
+ formatSelectionDays,
};
function availableStages(exercise) {
@@ -43,5 +79,64 @@ export default (config) => {
return applyFullApplicationSubmitted;
}
-
+
+ function applicationCounts(exercise) {
+ const applicationCounts = exercise && exercise._applications ? { ...exercise._applications } : {};
+ // include withdrawn applications in applied count
+ if (applicationCounts && applicationCounts.applied) {
+ applicationCounts.applied = applicationCounts.applied + (applicationCounts.withdrawn || 0);
+ }
+ return applicationCounts;
+ }
+
+ function shortlistingMethods(exercise) {
+ const methods = exercise.shortlistingMethods;
+ if (!(methods instanceof Array)) {
+ return [];
+ }
+ const list = methods.filter(value => (value !== 'other'));
+ list.sort();
+
+ if (methods.includes('other')) {
+ exercise.otherShortlistingMethod.forEach((method) => {
+ return list.push(method.name);
+ });
+ }
+
+ const lookupList = list.map((method) => {
+ return lookup(method);
+ });
+
+ return lookupList;
+ }
+
+ function formatSelectionDays(exercise) {
+ if (!exercise || !exercise.selectionDays) {
+ return [];
+ }
+
+ let dateStrings = [];
+
+ for (const selectionDay of exercise.selectionDays) {
+ let dateString = '';
+ const selectionDayStart = formatDate(selectionDay.selectionDayStart, 'DD/MM/YYYY');
+ const selectionDayEnd = formatDate(selectionDay.selectionDayEnd, 'DD/MM/YYYY');
+
+ if (!selectionDayStart || !selectionDayEnd) {
+ dateString = '';
+ } else if (selectionDayStart !== selectionDayEnd) {
+ dateString = `${selectionDayStart} to ${selectionDayEnd}`;
+ } else {
+ dateString = `${selectionDayStart}`;
+ }
+ if (!_.isEmpty(dateString)) {
+ if (selectionDay.selectionDayLocation) {
+ dateString = `${selectionDay.selectionDayLocation} - ${dateString}`;
+ }
+ dateStrings.push(dateString);
+ }
+ }
+
+ return dateStrings;
+ }
};
diff --git a/test/functions/exportSccSummaryReport.spec.js b/test/functions/exportSccSummaryReport.spec.js
new file mode 100644
index 000000000..92718bfc7
--- /dev/null
+++ b/test/functions/exportSccSummaryReport.spec.js
@@ -0,0 +1,34 @@
+import assert from 'assert';
+import { firebaseFunctionsTest, generateMockContext } from './helpers.js';
+import { PERMISSIONS } from '../../functions/shared/permissions.js';
+import exportSccSummaryReport from '../../functions/callableFunctions/exportSccSummaryReport.js';
+
+const { wrap } = firebaseFunctionsTest;
+
+describe('exportSccSummaryReport', () => {
+ context('Permission', () => {
+ it ('has no permission', async () => {
+ const wrapped = wrap(exportSccSummaryReport);
+ try {
+ await wrapped({}, generateMockContext());
+ } catch (e) {
+ assert.equal(e.code, 'permission-denied');
+ }
+ });
+ it ('has permission', async () => {
+ const wrapped = wrap(exportSccSummaryReport);
+ const context = generateMockContext({
+ permissions: [
+ PERMISSIONS.exercises.permissions.canReadExercises.value,
+ PERMISSIONS.applicationRecords.permissions.canReadApplicationRecords.value,
+ PERMISSIONS.applications.permissions.canReadApplications.value,
+ ],
+ });
+ try {
+ await wrapped({}, context);
+ } catch (e) {
+ assert.equal(e.code, 'invalid-argument');
+ }
+ });
+ });
+});
diff --git a/test/functions/generateSccSummaryReport.spec.js b/test/functions/generateSccSummaryReport.spec.js
new file mode 100644
index 000000000..996f4fbad
--- /dev/null
+++ b/test/functions/generateSccSummaryReport.spec.js
@@ -0,0 +1,34 @@
+import assert from 'assert';
+import { firebaseFunctionsTest, generateMockContext } from './helpers.js';
+import { PERMISSIONS } from '../../functions/shared/permissions.js';
+import generateSccSummaryReport from '../../functions/callableFunctions/generateSccSummaryReport.js';
+
+const { wrap } = firebaseFunctionsTest;
+
+describe('generateSccSummaryReport', () => {
+ context('Permission', () => {
+ it ('has no permission', async () => {
+ const wrapped = wrap(generateSccSummaryReport);
+ try {
+ await wrapped({}, generateMockContext());
+ } catch (e) {
+ assert.equal(e.code, 'permission-denied');
+ }
+ });
+ it ('has permission', async () => {
+ const wrapped = wrap(generateSccSummaryReport);
+ const context = generateMockContext({
+ permissions: [
+ PERMISSIONS.exercises.permissions.canReadExercises.value,
+ PERMISSIONS.applications.permissions.canReadApplications.value,
+ PERMISSIONS.applicationRecords.permissions.canReadApplicationRecords.value,
+ ],
+ });
+ try {
+ await wrapped({}, context);
+ } catch (e) {
+ assert.equal(e.code, 'invalid-argument');
+ }
+ });
+ });
+});
diff --git a/test/rules/3.exercises.spec.js b/test/rules/3.exercises.spec.js
index 0f5410bad..b64d30376 100644
--- a/test/rules/3.exercises.spec.js
+++ b/test/rules/3.exercises.spec.js
@@ -221,5 +221,69 @@ describe('Exercises', () => {
});
});
+ context('Write SCC summary reports', () => {
+
+ it('prevent un-authenticated user from writing SCC summary report', async () => {
+ const db = await setup({}, {'exercises/ex1': {}});
+ await assertFails(
+ db.collection('exercises').doc('ex1').collection('reports').doc('sccSummary').set({})
+ );
+ });
+
+ it('prevent authenticated user from writing SCC summary report', async () => {
+ const db = await setup({ uid: 'user1', email: 'user@email.com', email_verified: false }, {'exercises/ex1': {}});
+ await assertFails(
+ db.collection('exercises').doc('ex1').collection('reports').doc('sccSummary').set({})
+ );
+ });
+
+ it('prevent authenticated user with verified email from writing SCC summary report', async () => {
+ const db = await setup({ uid: 'user1', email: 'user@email.com', email_verified: true }, {'exercises/ex1': {}});
+ await assertFails(
+ db.collection('exercises').doc('ex1').collection('reports').doc('sccSummary').set({})
+ );
+ });
+ it('prevent authenticated user with un-verified JAC email from writing SCC summary report', async () => {
+ const db = await setup({ uid: 'user1', email: 'user@judicialappointments.digital', email_verified: false }, {'exercises/ex1': {}});
+ await assertFails(
+ db.collection('exercises').doc('ex1').collection('reports').doc('sccSummary').set({})
+ );
+ });
+
+ it('prevent authenticated user with verified @judicialappointments.digital email to write SCC summary report with no data', async () => {
+ const db = await setup({ uid: 'user1', email: 'user@judicialappointments.digital', email_verified: true }, {'exercises/ex1': {}});
+ await assertFails(
+ db.collection('exercises').doc('ex1').collection('reports').doc('sccSummary').set({})
+ );
+ });
+
+ it('prevent authenticated user with verified @judicialappointments.digital email but without permission from writing SCC summary report', async () => {
+ const db = await setup({ uid: 'user1', email: 'user@judicialappointments.digital', email_verified: true }, {'exercises/ex1': {}});
+ await assertFails(
+ db.collection('exercises').doc('ex1').collection('reports').doc('sccSummary').set({})
+ );
+ });
+
+ it('prevent authenticated user with verified @judicialappointments.gov.uk email but without permission from writing SCC summary report', async () => {
+ const db = await setup({ uid: 'user1', email: 'user@judicialappointments.gov.uk', email_verified: true }, {'exercises/ex1': {}});
+ await assertFails(
+ db.collection('exercises').doc('ex1').collection('reports').doc('sccSummary').set({})
+ );
+ });
+
+ it('allow authenticated user with verified @judicialappointments.digital email and permission to write SCC summary report', async () => {
+ const db = await setup({ uid: 'user1', email: 'user@judicialappointments.digital', email_verified: true, rp: [PERMISSIONS.exercises.permissions.canUpdateExercises.value] }, {'exercises/ex1': {}});
+ await assertSucceeds(
+ db.collection('exercises').doc('ex1').collection('reports').doc('sccSummary').set({})
+ );
+ });
+
+ it('allow authenticated user with verified @judicialappointments.gov.uk email and permission to write SCC summary report', async () => {
+ const db = await setup({ uid: 'user1', email: 'user@judicialappointments.gov.uk', email_verified: true, rp: [PERMISSIONS.exercises.permissions.canUpdateExercises.value] }, {'exercises/ex1': {}});
+ await assertSucceeds(
+ db.collection('exercises').doc('ex1').collection('reports').doc('sccSummary').set({})
+ );
+ });
+ });
});
diff --git a/test/shared/exerciseHelper.spec.js b/test/shared/exerciseHelper.spec.js
new file mode 100644
index 000000000..0c1e8d23c
--- /dev/null
+++ b/test/shared/exerciseHelper.spec.js
@@ -0,0 +1,139 @@
+import initExerciseHelper from '../../functions/shared/exerciseHelper.js';
+
+const config = {
+ EXERCISE_STAGE: {
+ REVIEW: 'review',
+ SHORTLISTED: 'shortlisted',
+ SELECTED: 'selected',
+ RECOMMENDED: 'recommended',
+ HANDOVER: 'handover',
+ },
+};
+
+const exerciseHelper = initExerciseHelper(config);
+
+describe('shortlistingMethods()', () => {
+ it('returns empty array when shortlistingMethods is not an array', () => {
+ const exercise = {
+ shortlistingMethods: 'not-an-array',
+ };
+ const result = exerciseHelper.shortlistingMethods(exercise);
+ expect(result).toEqual([]);
+ });
+
+ it('returns empty array when shortlistingMethods is undefined', () => {
+ const exercise = {};
+ const result = exerciseHelper.shortlistingMethods(exercise);
+ expect(result).toEqual([]);
+ });
+
+ it('returns sorted list of methods excluding "other"', () => {
+ const exercise = {
+ shortlistingMethods: ['other'],
+ otherShortlistingMethod: [{ name: 'Custom method' }],
+ };
+ const result = exerciseHelper.shortlistingMethods(exercise);
+ expect(result).toEqual(['Custom method']);
+ });
+
+
+
+ it('handles multiple methods', () => {
+ const exercise = {
+ shortlistingMethods: ['situational-judgement-qualifying-test', 'scenario-test-qualifying-test'],
+ otherShortlistingMethod: [],
+ };
+ const result = exerciseHelper.shortlistingMethods(exercise);
+ expect(result).toEqual(['Scenario test qualifying test (QT)', 'Situational judgement qualifying test (QT)']);
+ });
+});
+
+describe('formatSelectionDays()', () => {
+ it('returns empty array when exercise is undefined', () => {
+ const result = exerciseHelper.formatSelectionDays(undefined);
+ expect(result).toEqual([]);
+ });
+
+ it('returns empty array when selectionDays is undefined', () => {
+ const exercise = {};
+ const result = exerciseHelper.formatSelectionDays(exercise);
+ expect(result).toEqual([]);
+ });
+
+ it('formats single day selection with location', () => {
+ const exercise = {
+ selectionDays: [{
+ selectionDayStart: new Date('2024-01-15'),
+ selectionDayEnd: new Date('2024-01-15'),
+ selectionDayLocation: 'London',
+ }],
+ };
+ const result = exerciseHelper.formatSelectionDays(exercise);
+ expect(result).toEqual(['London - 15/1/2024']);
+ });
+
+ it('formats date range with location', () => {
+ const exercise = {
+ selectionDays: [{
+ selectionDayStart: new Date('2024-01-15'),
+ selectionDayEnd: new Date('2024-01-17'),
+ selectionDayLocation: 'Manchester',
+ }],
+ };
+ const result = exerciseHelper.formatSelectionDays(exercise);
+ expect(result).toEqual(['Manchester - 15/1/2024 to 17/1/2024']);
+ });
+
+ it('formats multiple selection days', () => {
+ const exercise = {
+ selectionDays: [
+ {
+ selectionDayStart: new Date('2024-01-15'),
+ selectionDayEnd: new Date('2024-01-15'),
+ selectionDayLocation: 'London',
+ },
+ {
+ selectionDayStart: new Date('2024-02-20'),
+ selectionDayEnd: new Date('2024-02-22'),
+ selectionDayLocation: 'Manchester',
+ },
+ ],
+ };
+ const result = exerciseHelper.formatSelectionDays(exercise);
+ expect(result).toEqual([
+ 'London - 15/1/2024',
+ 'Manchester - 20/2/2024 to 22/2/2024',
+ ]);
+ });
+
+ it('handles selection day without location', () => {
+ const exercise = {
+ selectionDays: [{
+ selectionDayStart: new Date('2024-01-15'),
+ selectionDayEnd: new Date('2024-01-15'),
+ }],
+ };
+ const result = exerciseHelper.formatSelectionDays(exercise);
+ expect(result).toEqual(['15/1/2024']);
+ });
+
+ it('handles invalid dates', () => {
+ const exercise = {
+ selectionDays: [{
+ selectionDayStart: null,
+ selectionDayEnd: new Date('2024-01-15'),
+ selectionDayLocation: 'London',
+ }],
+ };
+ const result = exerciseHelper.formatSelectionDays(exercise);
+ expect(result).toEqual([]);
+ });
+
+ it('handles empty selectionDays array', () => {
+ const exercise = {
+ selectionDays: [],
+ };
+ const result = exerciseHelper.formatSelectionDays(exercise);
+ expect(result).toEqual([]);
+ });
+});