diff --git a/backend/migrations/20240827102746-addLevel2CareCertificate.js b/backend/migrations/20240827102746-addLevel2CareCertificate.js new file mode 100644 index 0000000000..11d7ce9a3e --- /dev/null +++ b/backend/migrations/20240827102746-addLevel2CareCertificate.js @@ -0,0 +1,78 @@ +'use strict'; + +const table = { tableName: 'Worker', schema: 'cqc' }; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((transaction) => { + return Promise.all([ + queryInterface.addColumn( + table, + 'Level2CareCertificateValue', + { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + { transaction }, + ), + queryInterface.addColumn( + table, + 'Level2CareCertificateYear', + { + type: Sequelize.DataTypes.INTEGER, + allowNull: true, + }, + { transaction }, + ), + queryInterface.addColumn( + table, + 'Level2CareCertificateSavedAt', + { + type: Sequelize.DataTypes.DATE, + allowNull: true, + }, + { transaction }, + ), + queryInterface.addColumn( + table, + 'Level2CareCertificateChangedAt', + { + type: Sequelize.DataTypes.DATE, + allowNull: true, + }, + { transaction }, + ), + queryInterface.addColumn( + table, + 'Level2CareCertificateSavedBy', + { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + { transaction }, + ), + queryInterface.addColumn( + table, + 'Level2CareCertificateChangedBy', + { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + { transaction }, + ), + ]); + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((transaction) => { + return Promise.all([ + queryInterface.removeColumn(table, 'Level2CareCertificateValue', { transaction }), + queryInterface.removeColumn(table, 'Level2CareCertificateYear', { transaction }), + queryInterface.removeColumn(table, 'Level2CareCertificateSavedAt', { transaction }), + queryInterface.removeColumn(table, 'Level2CareCertificateChangedAt', { transaction }), + queryInterface.removeColumn(table, 'Level2CareCertificateSavedBy', { transaction }), + queryInterface.removeColumn(table, 'Level2CareCertificateChangedBy', { transaction }), + ]); + }); + }, +}; diff --git a/backend/server/models/classes/worker.js b/backend/server/models/classes/worker.js index d8b10e2a96..d4df1905a0 100644 --- a/backend/server/models/classes/worker.js +++ b/backend/server/models/classes/worker.js @@ -218,6 +218,12 @@ class Worker extends EntityValidator { return this._properties.get('CareCertificate') ? this._properties.get('CareCertificate').property : null; } + get level2CareCertificate() { + return this._properties.get('Level2CareCertificate') + ? this._properties.get('Level2CareCertificate').property + : null; + } + get approvedMentalHealthWorker() { return this._properties.get('ApprovedMentalHealthWorker') ? this._properties.get('ApprovedMentalHealthWorker').property diff --git a/backend/server/models/classes/worker/properties/level2CareCertificateProperty.js b/backend/server/models/classes/worker/properties/level2CareCertificateProperty.js new file mode 100644 index 0000000000..222a81cb58 --- /dev/null +++ b/backend/server/models/classes/worker/properties/level2CareCertificateProperty.js @@ -0,0 +1,106 @@ +// the level 2 care certificate property is an enumeration +const ChangePropertyPrototype = require('../../properties/changePrototype').ChangePropertyPrototype; + +const LEVEL_2_CARE_CERTIFICATE_TYPE = ['Yes, completed', 'Yes, started', 'No']; +exports.WorkerLevel2CareCertificateProperty = class WorkerLevel2CareCertificateProperty extends ( + ChangePropertyPrototype +) { + constructor() { + super('Level2CareCertificate'); + this._allowNull = true; + } + + static clone() { + return new WorkerLevel2CareCertificateProperty(); + } + + // concrete implementations + async restoreFromJson(document) { + const completedInYearSinceLevel2CareCertIntroduced = (year) => { + const yearLevel2CareCertificateIntroduced = 2024; + const thisYear = new Date().getFullYear(); + return year >= yearLevel2CareCertificateIntroduced && document.level2CareCertificate.year <= thisYear + ? true + : false; + }; + + if (document.level2CareCertificate) { + if ( + [LEVEL_2_CARE_CERTIFICATE_TYPE[1], LEVEL_2_CARE_CERTIFICATE_TYPE[2]].includes( + document.level2CareCertificate.value, + ) + ) { + this.property = { + value: document.level2CareCertificate.value, + year: null, + }; + return; + } + + if (document.level2CareCertificate.value === LEVEL_2_CARE_CERTIFICATE_TYPE[0]) { + if ( + document.level2CareCertificate.year && + Number.isInteger(document.level2CareCertificate.year) && + completedInYearSinceLevel2CareCertIntroduced(document.level2CareCertificate.year) + ) { + this.property = { + value: document.level2CareCertificate.value, + year: document.level2CareCertificate.year, + }; + } else { + this.property = { + value: document.level2CareCertificate.value, + year: null, + }; + } + return; + } + this.property = null; + } + } + + restorePropertyFromSequelize(document) { + let level2CareCertificate = { + value: document.Level2CareCertificateValue, + year: document.Level2CareCertificateYear, + }; + return level2CareCertificate; + } + + savePropertyToSequelize() { + return { + Level2CareCertificateValue: this.property.value, + Level2CareCertificateYear: + this.property.value === 'Yes, completed' && this.property.year ? this.property.year : null, + }; + } + + isEqual(currentValue, newValue) { + // not a simple (enum'd) string compare; if "Yes, completed", also need to compare the year (just an integer) + + if (currentValue && newValue && currentValue.value === 'Yes, completed') { + if (currentValue.year === newValue.year) { + return currentValue.value === newValue.value; + } + return false; + } + + return currentValue && newValue && currentValue.value === newValue.value; + } + + toJSON(withHistory = false, showPropertyHistoryOnly = true) { + if (!withHistory) { + // simple form + return { + level2CareCertificate: this.property, + }; + } + + return { + level2CareCertificate: { + currentValue: this.property, + ...this.changePropsToJSON(showPropertyHistoryOnly), + }, + }; + } +}; diff --git a/backend/server/models/classes/worker/workerProperties.js b/backend/server/models/classes/worker/workerProperties.js index 845ea85b63..fd08b7994c 100644 --- a/backend/server/models/classes/worker/workerProperties.js +++ b/backend/server/models/classes/worker/workerProperties.js @@ -48,6 +48,8 @@ const establishmentFkProperty = require('./properties/establishmentFkProperty'). const longTermAbsenceProperty = require('./properties/longTermAbsenceProperty').LongTermAbsenceProperty; const healthAndCareVisaProperty = require('./properties/healthAndCareVisa').HealthAndCareVisaProperty; const employedFromOutsideUkProperty = require('./properties/employedFromOutsideUk').EmployedFromOutsideUkProperty; +const level2CareCertificateProperty = + require('./properties/level2CareCertificateProperty').WorkerLevel2CareCertificateProperty; class WorkerPropertyManager { constructor() { @@ -78,6 +80,7 @@ class WorkerPropertyManager { this._thisManager.registerProperty(weeklyHoursContractedProperty); this._thisManager.registerProperty(annualHourlyPayProperty); this._thisManager.registerProperty(careCertificateProperty); + this._thisManager.registerProperty(level2CareCertificateProperty); this._thisManager.registerProperty(apprenticeshipProperty); this._thisManager.registerProperty(qualificationInSocialCareProperty); this._thisManager.registerProperty(socialCareQualificationProperty); @@ -90,7 +93,7 @@ class WorkerPropertyManager { this._thisManager.registerProperty(establishmentFkProperty); this._thisManager.registerProperty(longTermAbsenceProperty); this._thisManager.registerProperty(healthAndCareVisaProperty); - this._thisManager.registerProperty(employedFromOutsideUkProperty) + this._thisManager.registerProperty(employedFromOutsideUkProperty); } get manager() { diff --git a/backend/server/models/establishment.js b/backend/server/models/establishment.js index 2ce64e0108..b95c3224ee 100644 --- a/backend/server/models/establishment.js +++ b/backend/server/models/establishment.js @@ -1503,6 +1503,8 @@ module.exports = function (sequelize, DataTypes) { 'ApprovedMentalHealthWorkerValue', 'QualificationInSocialCareValue', 'OtherQualificationsValue', + 'Level2CareCertificateValue', + 'Level2CareCertificateYear', ], as: 'workers', where: { diff --git a/backend/server/models/worker.js b/backend/server/models/worker.js index c3a73d07c6..ae00616807 100644 --- a/backend/server/models/worker.js +++ b/backend/server/models/worker.js @@ -768,6 +768,37 @@ module.exports = function (sequelize, DataTypes) { allowNull: true, field: '"CareCertificateChangedBy"', }, + Level2CareCertificateValue: { + type: DataTypes.ENUM, + allowNull: true, + values: ['Yes, completed', 'Yes, started', 'No'], + field: '"Level2CareCertificateValue"', + }, + Level2CareCertificateYear: { + type: DataTypes.INTEGER, + allowNull: true, + field: '"Level2CareCertificateYear"', + }, + Level2CareCertificateSavedAt: { + type: DataTypes.DATE, + allowNull: true, + field: '"Level2CareCertificateSavedAt"', + }, + Level2CareCertificateChangedAt: { + type: DataTypes.DATE, + allowNull: true, + field: '"Level2CareCertificateChangedAt"', + }, + Level2CareCertificateSavedBy: { + type: DataTypes.TEXT, + allowNull: true, + field: '"Level2CareCertificateSavedBy"', + }, + Level2CareCertificateChangedBy: { + type: DataTypes.TEXT, + allowNull: true, + field: '"Level2CareCertificateChangedBy"', + }, HealthAndCareVisaValue: { type: DataTypes.ENUM, allowNull: true, diff --git a/backend/server/routes/establishments/bulkUpload/data/workerHeaders.js b/backend/server/routes/establishments/bulkUpload/data/workerHeaders.js index 4fe04c34dd..c016b83b4c 100644 --- a/backend/server/routes/establishments/bulkUpload/data/workerHeaders.js +++ b/backend/server/routes/establishments/bulkUpload/data/workerHeaders.js @@ -15,6 +15,7 @@ const workerHeaders = [ 'YEAROFENTRY', 'DISABLED', 'CARECERT', + 'L2CARECERT', 'RECSOURCE', 'HANDCVISA', 'INOUTUK', diff --git a/backend/server/routes/establishments/bulkUpload/download/workerCSV.js b/backend/server/routes/establishments/bulkUpload/download/workerCSV.js index 3425379c1a..6b76276803 100644 --- a/backend/server/routes/establishments/bulkUpload/download/workerCSV.js +++ b/backend/server/routes/establishments/bulkUpload/download/workerCSV.js @@ -32,7 +32,7 @@ const _convertYesNoDontKnow = (value) => { // takes the given Worker entity and writes it out to CSV string (one line) const toCSV = (establishmentId, entity, MAX_QUALIFICATIONS, downloadType) => { // ["LOCALESTID","UNIQUEWORKERID","STATUS","DISPLAYID","NINUMBER","POSTCODE","DOB","GENDER","ETHNICITY","NATIONALITY","BRITISHCITIZENSHIP","COUNTRYOFBIRTH","YEAROFENTRY","DISABLED", - // "CARECERT","RECSOURCE","HANDCVISA","INOUTUK","STARTDATE","STARTINSECT","APPRENTICE","EMPLSTATUS","ZEROHRCONT","DAYSSICK","SALARYINT","SALARY","HOURLYRATE","MAINJOBROLE","MAINJRDESC","CONTHOURS","AVGHOURS", + // "CARECERT","L2CARECERT","RECSOURCE","HANDCVISA","INOUTUK","STARTDATE","STARTINSECT","APPRENTICE","EMPLSTATUS","ZEROHRCONT","DAYSSICK","SALARYINT","SALARY","HOURLYRATE","MAINJOBROLE","MAINJRDESC","CONTHOURS","AVGHOURS", // "NMCREG","NURSESPEC","AMHP","SCQUAL","NONSCQUAL","QUALACH01","QUALACH01NOTES","QUALACH02","QUALACH02NOTES","QUALACH03","QUALACH03NOTES"]; const columns = []; @@ -175,6 +175,25 @@ const toCSV = (establishmentId, entity, MAX_QUALIFICATIONS, downloadType) => { } columns.push(careCert); + // "L2CARECERT" + let l2CareCert = ''; + switch (entity.Level2CareCertificateValue) { + case 'Yes, completed': + if (entity.Level2CareCertificateYear) { + l2CareCert = `1;${entity.Level2CareCertificateYear}`; + } else { + l2CareCert = '1;'; + } + break; + case 'Yes, started': + l2CareCert = '2'; + break; + case 'No': + l2CareCert = '3'; + break; + } + columns.push(l2CareCert); + // "RECSOURCE" let recruitmentSource = ''; switch (entity.RecruitedFromValue) { diff --git a/backend/server/test/integration/utils/worker.js b/backend/server/test/integration/utils/worker.js index 9f0ab7e97b..ca08ec7920 100644 --- a/backend/server/test/integration/utils/worker.js +++ b/backend/server/test/integration/utils/worker.js @@ -39,6 +39,7 @@ module.exports.apiWorkerBuilder = build('Worker', { YearArrivedValue: oneOf('Yes', 'No', "Don't know"), DisabilityValue: oneOf('Yes', 'No', "Don't know", 'Undisclosed'), CareCertificateValue: oneOf('Yes, completed', 'No', 'Yes, in progress or partially completed'), + Level2CareCertificateValue: oneOf('Yes, completed', 'Yes, started', 'No'), RecruitedFromValue: oneOf('Yes', 'No'), MainJobStartDateValue: fake((f) => f.helpers.replaceSymbolWithNumber('####-##-##')), recruitedFrom: { diff --git a/backend/server/test/unit/mockdata/workers.js b/backend/server/test/unit/mockdata/workers.js index 5c572cb938..91dcc4a785 100644 --- a/backend/server/test/unit/mockdata/workers.js +++ b/backend/server/test/unit/mockdata/workers.js @@ -150,6 +150,7 @@ exports.knownHeaders = [ 'YEAROFENTRY', 'DISABLED', 'CARECERT', + 'L2CARECERT', 'RECSOURCE', 'HANDCVISA', 'INOUTUK', diff --git a/backend/server/test/unit/models/classes/worker/properties/level2CareCertificateProperty.spec.js b/backend/server/test/unit/models/classes/worker/properties/level2CareCertificateProperty.spec.js new file mode 100644 index 0000000000..3814abc5cb --- /dev/null +++ b/backend/server/test/unit/models/classes/worker/properties/level2CareCertificateProperty.spec.js @@ -0,0 +1,204 @@ +const expect = require('chai').expect; +const level2CareCertificatePropertyClass = + require('../../../../../../models/classes/worker/properties/level2CareCertificateProperty').WorkerLevel2CareCertificateProperty; + +const level2CareCertificateValues = [ + { value: 'Yes, completed', year: null }, + { value: 'Yes, completed', year: 2024 }, + { value: 'Yes, started' }, + { value: 'No' }, +]; + +const level2CareCertificateReturnedValues = [ + { value: 'Yes, completed', year: null }, + { value: 'Yes, completed', year: 2024 }, + { value: 'Yes, started', year: null }, + { value: 'No', year: null }, +]; + +describe('level2CareCertificate Property', () => { + describe('restoreFromJSON', async () => { + it("shouldn't return anything if undefined", async () => { + const level2CareCertificateProperty = new level2CareCertificatePropertyClass(); + const document = {}; + await level2CareCertificateProperty.restoreFromJson(document); + expect(level2CareCertificateProperty.property).to.deep.equal(null); + }); + + await Promise.all( + level2CareCertificateValues.map(async (level2CareCertificate, index) => { + it( + 'should return with correct value for ' + + level2CareCertificate.value + + ' and year ' + + level2CareCertificate?.year, + async () => { + const level2CareCertificateProperty = new level2CareCertificatePropertyClass(); + + const document = { + level2CareCertificate, + }; + await level2CareCertificateProperty.restoreFromJson(document); + expect(level2CareCertificateProperty.property).to.deep.equal(level2CareCertificateReturnedValues[index]); + }, + ); + }), + ); + }); + + describe('restorePropertyFromSequelize()', async () => { + it("shouldn't return anything if undefined", async () => { + const level2CareCertificateProperty = new level2CareCertificatePropertyClass(); + const document = {}; + const level2CareCertificate = await level2CareCertificateProperty.restorePropertyFromSequelize(document); + expect(level2CareCertificate).to.deep.equal({ value: undefined, year: undefined }); + }); + + await Promise.all( + level2CareCertificateValues.map(async (level2CareCertificate, index) => { + it( + 'should return with correct value for ' + + level2CareCertificate.value + + ' and year ' + + level2CareCertificate?.year, + async () => { + const level2CareCertificateProperty = new level2CareCertificatePropertyClass(); + + const document = { + Level2CareCertificateValue: level2CareCertificate.value, + Level2CareCertificateYear: level2CareCertificate.year ? level2CareCertificate.year : null, + }; + const level2CareCertificateProps = await level2CareCertificateProperty.restorePropertyFromSequelize( + document, + ); + expect(level2CareCertificateProps).to.deep.equal(level2CareCertificateReturnedValues[index]); + }, + ); + }), + ); + }); + + describe('savePropertyToSequelize', async () => { + await Promise.all( + level2CareCertificateValues.map(async (level2CareCertificate, index) => { + it( + 'should save in correct format as if saving into database for ' + + level2CareCertificate.value + + ' and year ' + + level2CareCertificate?.year, + async () => { + const level2CareCertificateProperty = new level2CareCertificatePropertyClass(); + + const document = { + Level2CareCertificateValue: level2CareCertificate.value, + Level2CareCertificateYear: level2CareCertificate.year ? level2CareCertificate.year : null, + }; + const level2CareCertificateProps = await level2CareCertificateProperty.restorePropertyFromSequelize( + document, + ); + expect(level2CareCertificateProps).to.deep.equal(level2CareCertificateReturnedValues[index]); + }, + ); + }), + ); + }); + + describe('isEqual()', () => { + it('should return true if the values are equal and the year is the same', () => { + const level2CareCertificateProperty = new level2CareCertificatePropertyClass(); + + const equal = level2CareCertificateProperty.isEqual( + level2CareCertificateValues[1], + level2CareCertificateReturnedValues[1], + ); + expect(equal).to.deep.equal(true); + }); + + it('should return true if the values are equal and there is no year', () => { + const level2CareCertificateProperty = new level2CareCertificatePropertyClass(); + + const equal = level2CareCertificateProperty.isEqual( + level2CareCertificateValues[0], + level2CareCertificateReturnedValues[0], + ); + expect(equal).to.deep.equal(true); + }); + + it(`should return true if the values are the same when ${level2CareCertificateValues[2].value}`, () => { + const level2CareCertificateProperty = new level2CareCertificatePropertyClass(); + + const equal = level2CareCertificateProperty.isEqual( + level2CareCertificateValues[2], + level2CareCertificateReturnedValues[2], + ); + expect(equal).to.deep.equal(true); + }); + + it(`should return true if the values are the same when ${level2CareCertificateValues[3].value}`, () => { + const level2CareCertificateProperty = new level2CareCertificatePropertyClass(); + + const equal = level2CareCertificateProperty.isEqual( + level2CareCertificateValues[3], + level2CareCertificateReturnedValues[3], + ); + expect(equal).to.deep.equal(true); + }); + + it(`should return false if the values different (from ${level2CareCertificateValues[0].value} to ${level2CareCertificateReturnedValues[2].value})`, () => { + const level2CareCertificateProperty = new level2CareCertificatePropertyClass(); + + const equal = level2CareCertificateProperty.isEqual( + level2CareCertificateValues[0], + level2CareCertificateReturnedValues[2], + ); + expect(equal).to.deep.equal(false); + }); + + it(`should return false if the values different (from ${level2CareCertificateValues[0].value} to ${level2CareCertificateReturnedValues[3].value})`, () => { + const level2CareCertificateProperty = new level2CareCertificatePropertyClass(); + + const equal = level2CareCertificateProperty.isEqual( + level2CareCertificateValues[0], + level2CareCertificateReturnedValues[3], + ); + expect(equal).to.deep.equal(false); + }); + + it('should return false when there is a current year is but the new year sent is null', () => { + const level2CareCertificateProperty = new level2CareCertificatePropertyClass(); + const equal = level2CareCertificateProperty.isEqual( + level2CareCertificateValues[1], + level2CareCertificateReturnedValues[0], + ); + expect(equal).to.deep.equal(false); + }); + + it('should return false when there is no current year is but there is a new year sent', () => { + const level2CareCertificateProperty = new level2CareCertificatePropertyClass(); + const equal = level2CareCertificateProperty.isEqual( + level2CareCertificateValues[0], + level2CareCertificateValues[1], + ); + expect(equal).to.deep.equal(false); + }); + }); + + describe('toJSON()', () => { + level2CareCertificateValues.map((level2CCValue, index) => { + it( + 'should return correctly formatted JSON for ' + level2CCValue.value + ' and year ' + level2CCValue?.year, + () => { + const level2CareCertificateProperty = new level2CareCertificatePropertyClass(); + + const level2CareCertificate = { + year: level2CCValue.value, + value: level2CCValue.year ? level2CCValue.year : null, + }; + level2CareCertificateProperty.property = level2CareCertificate; + const json = level2CareCertificateProperty.toJSON(); + expect(json.level2CareCertificate).to.deep.equal(level2CareCertificate); + }, + ); + }); + }); +}); diff --git a/backend/server/test/unit/routes/establishments/bulkUpload/download/workerCSV.spec.js b/backend/server/test/unit/routes/establishments/bulkUpload/download/workerCSV.spec.js index 92de6ae763..cee32ac01f 100644 --- a/backend/server/test/unit/routes/establishments/bulkUpload/download/workerCSV.spec.js +++ b/backend/server/test/unit/routes/establishments/bulkUpload/download/workerCSV.spec.js @@ -308,6 +308,31 @@ describe('workerCSV', () => { expect(csvAsArray[getWorkerColumnIndex('CARECERT')]).to.equal(careCert.code); }); }); + + [ + { name: 'Yes, completed', code: '1;' }, + { name: 'Yes, completed', year: 2024, code: '1;2024' }, + { name: 'Yes, started', code: '2' }, + { name: 'No', code: '3' }, + ].forEach((level2CareCert) => { + let testName = 'should return the correct code for level 2 care certificate ' + level2CareCert.name; + if (level2CareCert.year) { + testName += ` with year: ${level2CareCert.year}`; + } + + it(testName, async () => { + worker.Level2CareCertificateValue = level2CareCert.name; + if (level2CareCert.year) { + worker.Level2CareCertificateYear = level2CareCert.year; + } + + const csv = toCSV(establishment.LocalIdentifierValue, worker, 3); + const csvAsArray = csv.split(','); + + expect(csvAsArray[getWorkerColumnIndex('L2CARECERT')]).to.equal(level2CareCert.code); + }); + }); + it('should return the correct code for no in recruitment source', async () => { worker.RecruitedFromValue = 'No'; worker.recruitedFrom = null; diff --git a/backend/server/test/unit/routes/establishments/bulkUpload/uploadFiles.spec.js b/backend/server/test/unit/routes/establishments/bulkUpload/uploadFiles.spec.js index 122ed4cd2f..5f9a0dab8c 100644 --- a/backend/server/test/unit/routes/establishments/bulkUpload/uploadFiles.spec.js +++ b/backend/server/test/unit/routes/establishments/bulkUpload/uploadFiles.spec.js @@ -16,7 +16,7 @@ describe('/server/routes/establishment/uploadFiles.js', () => { const EstablishmentFile = 'LOCALESTID,STATUS,ESTNAME,ADDRESS1,ADDRESS2,ADDRESS3,POSTTOWN,POSTCODE,ESTTYPE,OTHERTYPE,PERMCQC,PERMLA,REGTYPE,PROVNUM,LOCATIONID,MAINSERVICE,ALLSERVICES,CAPACITY,UTILISATION,SERVICEDESC,SERVICEUSERS,OTHERUSERDESC,TOTALPERMTEMP,ALLJOBROLES,STARTERS,LEAVERS,VACANCIES,REASONS,REASONNOS,ADVERTISING,INTERVIEWS,REPEATTRAINING,ACCEPTCARECERT,BENEFITS,SICKPAY,PENSION,HOLIDAY'; const WorkerFile = - 'LOCALESTID,UNIQUEWORKERID,STATUS,DISPLAYID,NINUMBER,POSTCODE,DOB,GENDER,ETHNICITY,NATIONALITY,BRITISHCITIZENSHIP,COUNTRYOFBIRTH,YEAROFENTRY,DISABLED,CARECERT,RECSOURCE,HANDCVISA,INOUTUK,STARTDATE,STARTINSECT,APPRENTICE,EMPLSTATUS,ZEROHRCONT,DAYSSICK,SALARYINT,SALARY,HOURLYRATE,MAINJOBROLE,MAINJRDESC,CONTHOURS,AVGHOURS,NMCREG,NURSESPEC,AMHP,SCQUAL,NONSCQUAL,QUALACH01,QUALACH01NOTES,QUALACH02,QUALACH02NOTES,QUALACH03,QUALACH03NOTES'; + 'LOCALESTID,UNIQUEWORKERID,STATUS,DISPLAYID,NINUMBER,POSTCODE,DOB,GENDER,ETHNICITY,NATIONALITY,BRITISHCITIZENSHIP,COUNTRYOFBIRTH,YEAROFENTRY,DISABLED,CARECERT,L2CARECERT,RECSOURCE,HANDCVISA,INOUTUK,STARTDATE,STARTINSECT,APPRENTICE,EMPLSTATUS,ZEROHRCONT,DAYSSICK,SALARYINT,SALARY,HOURLYRATE,MAINJOBROLE,MAINJRDESC,CONTHOURS,AVGHOURS,NMCREG,NURSESPEC,AMHP,SCQUAL,NONSCQUAL,QUALACH01,QUALACH01NOTES,QUALACH02,QUALACH02NOTES,QUALACH03,QUALACH03NOTES'; const OtherFile = 'Test,This,is,NOT,A,BULK,UPLOAD,FILE'; sinon.stub(S3.s3, 'putObject').returns({ diff --git a/backend/server/test/unit/routes/establishments/bulkUpload/validate/headers/worker.spec.js b/backend/server/test/unit/routes/establishments/bulkUpload/validate/headers/worker.spec.js index 938b4d6b72..9e52900a63 100644 --- a/backend/server/test/unit/routes/establishments/bulkUpload/validate/headers/worker.spec.js +++ b/backend/server/test/unit/routes/establishments/bulkUpload/validate/headers/worker.spec.js @@ -7,7 +7,7 @@ const expect = require('chai').expect; const workerHeadersWithCHGUNIQUEWRKID = 'LOCALESTID,UNIQUEWORKERID,CHGUNIQUEWRKID,STATUS,DISPLAYID,NINUMBER,' + 'POSTCODE,DOB,GENDER,ETHNICITY,NATIONALITY,BRITISHCITIZENSHIP,COUNTRYOFBIRTH,YEAROFENTRY,' + - 'DISABLED,CARECERT,RECSOURCE,HANDCVISA,INOUTUK,STARTDATE,STARTINSECT,APPRENTICE,EMPLSTATUS,ZEROHRCONT,' + + 'DISABLED,CARECERT,L2CARECERT,RECSOURCE,HANDCVISA,INOUTUK,STARTDATE,STARTINSECT,APPRENTICE,EMPLSTATUS,ZEROHRCONT,' + 'DAYSSICK,SALARYINT,SALARY,HOURLYRATE,MAINJOBROLE,MAINJRDESC,CONTHOURS,AVGHOURS,' + 'NMCREG,NURSESPEC,AMHP,SCQUAL,NONSCQUAL,QUALACH01,QUALACH01NOTES,' + 'QUALACH02,QUALACH02NOTES,QUALACH03,QUALACH03NOTES'; @@ -15,7 +15,7 @@ const workerHeadersWithCHGUNIQUEWRKID = const workerHeadersWithoutCHGUNIQUEWRKID = 'LOCALESTID,UNIQUEWORKERID,STATUS,DISPLAYID,NINUMBER,' + 'POSTCODE,DOB,GENDER,ETHNICITY,NATIONALITY,BRITISHCITIZENSHIP,COUNTRYOFBIRTH,YEAROFENTRY,' + - 'DISABLED,CARECERT,RECSOURCE,HANDCVISA,INOUTUK,STARTDATE,STARTINSECT,APPRENTICE,EMPLSTATUS,ZEROHRCONT,' + + 'DISABLED,CARECERT,L2CARECERT,RECSOURCE,HANDCVISA,INOUTUK,STARTDATE,STARTINSECT,APPRENTICE,EMPLSTATUS,ZEROHRCONT,' + 'DAYSSICK,SALARYINT,SALARY,HOURLYRATE,MAINJOBROLE,MAINJRDESC,CONTHOURS,AVGHOURS,' + 'NMCREG,NURSESPEC,AMHP,SCQUAL,NONSCQUAL,QUALACH01,QUALACH01NOTES,' + 'QUALACH02,QUALACH02NOTES,QUALACH03,QUALACH03NOTES'; diff --git a/frontend/src/app/core/model/worker.model.ts b/frontend/src/app/core/model/worker.model.ts index 58e71396eb..bd59a866df 100644 --- a/frontend/src/app/core/model/worker.model.ts +++ b/frontend/src/app/core/model/worker.model.ts @@ -68,6 +68,10 @@ export interface Worker { }; annualHourlyPay: WorkerPay; careCertificate: string; + level2CareCertificate?: { + value: string; + year?: number; + }; apprenticeshipTraining: string; qualificationInSocialCare: string; socialCareQualification: { diff --git a/frontend/src/app/core/test-utils/MockWorkerService.ts b/frontend/src/app/core/test-utils/MockWorkerService.ts index e4bd442568..b429a781aa 100644 --- a/frontend/src/app/core/test-utils/MockWorkerService.ts +++ b/frontend/src/app/core/test-utils/MockWorkerService.ts @@ -33,6 +33,10 @@ export const workerBuilder = build('Worker', { rate: 8.98, }, careCertificate: 'Yes', + level2CareCertificate: { + value: 'Yes, completed', + year: 2023, + }, apprenticeshipTraining: null, qualificationInSocialCare: 'No', otherQualification: 'Yes', diff --git a/frontend/src/app/features/training-and-qualifications/new-training-qualifications-record/new-qualifications/new-qualifications.component.html b/frontend/src/app/features/training-and-qualifications/new-training-qualifications-record/new-qualifications/new-qualifications.component.html index bded6b1ddb..4d7862ca28 100644 --- a/frontend/src/app/features/training-and-qualifications/new-training-qualifications-record/new-qualifications/new-qualifications.component.html +++ b/frontend/src/app/features/training-and-qualifications/new-training-qualifications-record/new-qualifications/new-qualifications.component.html @@ -13,7 +13,6 @@

Qualifications

Certificate name Year achieved - Notes @@ -34,9 +33,6 @@

Qualifications

{{ qualificationRecord?.year ? qualificationRecord.year : '-' }} - - {{ qualificationRecord?.notes ? qualificationRecord.notes : 'None' }} - diff --git a/frontend/src/app/features/training-and-qualifications/new-training-qualifications-record/new-qualifications/new-qualifications.spec.ts b/frontend/src/app/features/training-and-qualifications/new-training-qualifications-record/new-qualifications/new-qualifications.spec.ts index 1ddeef8433..22ed7a4d2d 100644 --- a/frontend/src/app/features/training-and-qualifications/new-training-qualifications-record/new-qualifications/new-qualifications.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/new-training-qualifications-record/new-qualifications/new-qualifications.spec.ts @@ -39,7 +39,6 @@ describe('NewQualificationsComponent', () => { expect(getAllByText('Certificate name').length).toBe(2); expect(getAllByText('Year achieved').length).toBe(2); - expect(getAllByText('Notes').length).toBe(2); }); it('should show Health table row with details of record', async () => { @@ -47,7 +46,6 @@ describe('NewQualificationsComponent', () => { expect(getByText('Health qualification')).toBeTruthy(); expect(getByText('2020')).toBeTruthy(); - expect(getByText('This is a test note for the first row in the Health group')).toBeTruthy(); }); it('should show Certificate table first row with details of record', async () => { @@ -55,7 +53,6 @@ describe('NewQualificationsComponent', () => { expect(getByText('Cert qualification')).toBeTruthy(); expect(getByText('2021')).toBeTruthy(); - expect(getByText('Test notes needed')).toBeTruthy(); }); it('should show Certificate table second row with details of record', async () => { @@ -63,7 +60,6 @@ describe('NewQualificationsComponent', () => { expect(getByText('Another name for qual')).toBeTruthy(); expect(getByText('2012')).toBeTruthy(); - expect(getByText('These are some more notes in the second row of the cert table')).toBeTruthy(); }); describe('no qualifications', async () => { diff --git a/frontend/src/app/features/wdf/wdf-data-change/wdf-routing.module.ts b/frontend/src/app/features/wdf/wdf-data-change/wdf-routing.module.ts index b26f741f2e..22236acb8d 100644 --- a/frontend/src/app/features/wdf/wdf-data-change/wdf-routing.module.ts +++ b/frontend/src/app/features/wdf/wdf-data-change/wdf-routing.module.ts @@ -40,6 +40,7 @@ import { WdfDataComponent } from './wdf-data/wdf-data.component'; import { WdfOverviewComponent } from './wdf-overview/wdf-overview.component'; import { WdfStaffRecordComponent } from './wdf-staff-record/wdf-staff-record.component'; import { WdfWorkplacesSummaryComponent } from './wdf-workplaces-summary/wdf-workplaces-summary.component'; +import { Level2AdultSocialCareCertificateComponent } from '@features/workers/level-2-adult-social-care-certificate/level-2-adult-social-care-certificate.component'; const routes: Routes = [ { @@ -200,6 +201,11 @@ const routes: Routes = [ component: CareCertificateComponent, data: { title: 'Care Certificate' }, }, + { + path: 'level-2-care-certificate', + component: Level2AdultSocialCareCertificateComponent, + data: { title: 'Level 2 Adult Social Care Certificate' }, + }, { path: 'apprenticeship-training', component: ApprenticeshipTrainingComponent, @@ -389,6 +395,11 @@ const routes: Routes = [ component: CareCertificateComponent, data: { title: 'Care Certificate' }, }, + { + path: 'level-2-care-certificate', + component: Level2AdultSocialCareCertificateComponent, + data: { title: 'Level 2 Adult Social Care Certificate' }, + }, { path: 'apprenticeship-training', component: ApprenticeshipTrainingComponent, diff --git a/frontend/src/app/features/workers/care-certificate/care-certificate.component.html b/frontend/src/app/features/workers/care-certificate/care-certificate.component.html index ca29a30213..5eb1e7f461 100644 --- a/frontend/src/app/features/workers/care-certificate/care-certificate.component.html +++ b/frontend/src/app/features/workers/care-certificate/care-certificate.component.html @@ -11,6 +11,21 @@

Have they completed, started or partially completed their Care Certificate?

+
+ The Care Certificate is not the same thing as the Level 2 Adult Social Care Certificate, introduced in 2024. +
+
+ + What’s the Care Certificate? + +
+

+ The Care Certificate is an agreed set of standards that define the knowledge, skills and behaviours + expected of specific job roles in the health and social care sectors. It’s made up of the 15 standards + that should be covered as part of a robust induction programme. +

+
+
{ expect(getByLabelText('No')).toBeTruthy(); }); + it('should render a inset text to explain Care Certificate is not the same as L2 CC certificate', async () => { + const { getByText } = await setup(); + + const explanationText = getByText( + 'The Care Certificate is not the same thing as the Level 2 Adult Social Care Certificate, introduced in 2024.', + ); + + expect(explanationText).toBeTruthy(); + }); + + it('should render a reveal text about what is the Care Certification', async () => { + const { getByText } = await setup(); + + const reveal = getByText('What’s the Care Certificate?'); + const revealText = getByText( + 'The Care Certificate is an agreed set of standards that define the knowledge, skills and behaviours expected of specific job roles in the health and social care sectors. It’s made up of the 15 standards that should be covered as part of a robust induction programme.', + ); + + expect(reveal).toBeTruthy(); + expect(revealText).toBeTruthy(); + }); + describe('submit buttons', () => { it(`should show 'Save and continue' cta button, skip this question and 'View this staff record' link, if a return url is not provided`, async () => { const { getByText } = await setup(); @@ -110,7 +132,7 @@ describe('CareCertificateComponent', () => { }); describe('navigation', () => { - it('should navigate to apprenticeship-training page when submitting from flow', async () => { + it('should navigate to level-2-care-certificate page when submitting from flow', async () => { const { component, routerSpy, getByText } = await setup(); const workerId = component.worker.uid; @@ -126,11 +148,11 @@ describe('CareCertificateComponent', () => { workplaceId, 'staff-record', workerId, - 'apprenticeship-training', + 'level-2-care-certificate', ]); }); - it('should navigate to apprenticeship-training page when skipping the question in the flow', async () => { + it('should navigate to level-2-care-certificate page when skipping the question in the flow', async () => { const { component, routerSpy, getByText } = await setup(); const workerId = component.worker.uid; @@ -144,7 +166,7 @@ describe('CareCertificateComponent', () => { workplaceId, 'staff-record', workerId, - 'apprenticeship-training', + 'level-2-care-certificate', ]); }); diff --git a/frontend/src/app/features/workers/care-certificate/care-certificate.component.ts b/frontend/src/app/features/workers/care-certificate/care-certificate.component.ts index 19ad711575..4961b3d7cc 100644 --- a/frontend/src/app/features/workers/care-certificate/care-certificate.component.ts +++ b/frontend/src/app/features/workers/care-certificate/care-certificate.component.ts @@ -37,7 +37,7 @@ export class CareCertificateComponent extends QuestionComponent { } init() { - this.next = this.getRoutePath('apprenticeship-training'); + this.next = this.getRoutePath('level-2-care-certificate'); if (this.worker.careCertificate) { this.prefill(); } diff --git a/frontend/src/app/features/workers/level-2-adult-social-care-certificate/level-2-adult-social-care-certificate.component.html b/frontend/src/app/features/workers/level-2-adult-social-care-certificate/level-2-adult-social-care-certificate.component.html new file mode 100644 index 0000000000..f5e99e3200 --- /dev/null +++ b/frontend/src/app/features/workers/level-2-adult-social-care-certificate/level-2-adult-social-care-certificate.component.html @@ -0,0 +1,121 @@ + + +
+
+
+
+
+ +

+ {{ section }} + Have they completed or started their Level 2 Adult Social Care Certificate? +

+
+ +
+ The Level 2 Adult Social Care Certificate qualification, introduced in 2024, is not the same thing as the + existing standards based Care Certificate. +
+ + +

+ The Level 2 Adult Social Care Certificate is a qualification based on the existing Care Certificate + standards. It was introduced into the adult social care sector in 2024 as a new qualification for care + workers to work towards. +

+
+
+
+ + +
+ +
+
+ + + + Error: + {{ getFormErrorMessage('level2CareCertificateYearAchieved', 'min') }} + + + Error: + {{ getFormErrorMessage('level2CareCertificateYearAchieved', 'max') }} + + + +
+
+
+ + +
+
+ + +
+
+
+
+
+
+
+ +
+
+
+ + +
diff --git a/frontend/src/app/features/workers/level-2-adult-social-care-certificate/level-2-adult-social-care-certificate.component.spec.ts b/frontend/src/app/features/workers/level-2-adult-social-care-certificate/level-2-adult-social-care-certificate.component.spec.ts new file mode 100644 index 0000000000..39ff83c334 --- /dev/null +++ b/frontend/src/app/features/workers/level-2-adult-social-care-certificate/level-2-adult-social-care-certificate.component.spec.ts @@ -0,0 +1,409 @@ +import { getTestBed } from '@angular/core/testing'; +import { fireEvent, render } from '@testing-library/angular'; +import { SharedModule } from '@shared/shared.module'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms'; +import { WorkerService } from '@core/services/worker.service'; +import { Worker } from '@core/model/worker.model'; +import { MockWorkerServiceWithUpdateWorker, workerBuilder } from '@core/test-utils/MockWorkerService'; + +import { Level2AdultSocialCareCertificateComponent } from './level-2-adult-social-care-certificate.component'; +import { HttpClient } from '@angular/common/http'; +import dayjs from 'dayjs'; + +describe('Level2AdultSocialCareCertificateComponent', () => { + const workerFieldsNoLevel2CareCertificate = { level2CareCertificate: { value: null, year: null } }; + async function setup(insideFlow = true, workerFields = workerFieldsNoLevel2CareCertificate) { + const { fixture, getByText, getAllByText, getByLabelText, getByTestId, queryByTestId, queryByText } = await render( + Level2AdultSocialCareCertificateComponent, + { + imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule, ReactiveFormsModule], + providers: [ + UntypedFormBuilder, + { + provide: ActivatedRoute, + useValue: { + parent: { + snapshot: { + url: [{ path: insideFlow ? 'staff-uid' : 'staff-record-summary' }], + data: { + establishment: { uid: 'mocked-uid' }, + primaryWorkplace: {}, + }, + }, + }, + snapshot: { + params: {}, + }, + }, + }, + { + provide: WorkerService, + useFactory: MockWorkerServiceWithUpdateWorker.factory({ ...workerBuilder(), ...workerFields } as Worker), + deps: [HttpClient], + }, + ], + declarations: [], + }, + ); + const injector = getTestBed(); + + const component = fixture.componentInstance; + + const router = injector.inject(Router) as Router; + + const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + + return { + component, + fixture, + getByText, + getAllByText, + getByLabelText, + getByTestId, + queryByTestId, + queryByText, + router, + routerSpy, + }; + } + + it('should render the Level2AdultSocialCareCertificateComponent', async () => { + const { component } = await setup(); + expect(component).toBeTruthy(); + }); + + it('should show the caption', async () => { + const { getByTestId } = await setup(); + + const sectionHeading = getByTestId('section-heading'); + const captionText = 'Training and qualifications'; + + expect(sectionHeading.textContent).toEqual(captionText); + }); + + it('should show the heading', async () => { + const { getByText } = await setup(); + + const headingText = getByText('Have they completed or started their Level 2 Adult Social Care Certificate?'); + + expect(headingText).toBeTruthy(); + }); + + it('should show the reveal', async () => { + const { getByTestId } = await setup(); + + const reveal = getByTestId('reveal-whatIsLevel2CC'); + + expect(reveal).toBeTruthy(); + }); + + it('should show the radio buttons', async () => { + const { getByLabelText } = await setup(); + + expect(getByLabelText('Yes, completed')).toBeTruthy(); + expect(getByLabelText('Yes, started')).toBeTruthy(); + expect(getByLabelText('No')).toBeTruthy(); + }); + + describe('submit buttons', () => { + it(`should show 'Save and continue' cta button, skip this question and 'View this staff record' link, if a return url is not provided`, async () => { + const { getByText } = await setup(); + + expect(getByText('Save and continue')).toBeTruthy(); + expect(getByText('Skip this question')).toBeTruthy(); + expect(getByText('View this staff record')).toBeTruthy(); + }); + + it(`should show 'Save and return' cta button and 'Cancel' link if a return url is provided`, async () => { + const { getByText } = await setup(false); + + expect(getByText('Save and return')).toBeTruthy(); + expect(getByText('Cancel')).toBeTruthy(); + }); + }); + + describe('progress bar', () => { + it('should render the workplace progress bar', async () => { + const { getByTestId } = await setup(); + + expect(getByTestId('progress-bar')).toBeTruthy(); + }); + + it('should not render the progress bars when accessed from outside the flow', async () => { + const { queryByTestId } = await setup(false); + + expect(queryByTestId('progress-bar')).toBeFalsy(); + }); + }); + + describe('year achieved input', () => { + describe('should not show', () => { + it('when the page is loaded', async () => { + const { fixture } = await setup(); + + const yearAchievedInput = fixture.nativeElement.querySelector('div[id="certification-achieved"]'); + + expect(yearAchievedInput.getAttribute('class')).toContain('hidden'); + }); + + it('when "Yes, started" is clicked', async () => { + const { fixture, getByLabelText } = await setup(); + + const yesStartedRadioButton = getByLabelText('Yes, started'); + + fireEvent.click(yesStartedRadioButton); + fixture.detectChanges(); + + const yearAchievedInput = fixture.nativeElement.querySelector('div[id="certification-achieved"]'); + + expect(yearAchievedInput.getAttribute('class')).toContain('hidden'); + }); + + it('when "no" is clicked', async () => { + const { fixture, getByLabelText } = await setup(); + + const noRadioButton = getByLabelText('No'); + + fireEvent.click(noRadioButton); + fixture.detectChanges(); + + const yearAchievedInput = fixture.nativeElement.querySelector('div[id="certification-achieved"]'); + + expect(yearAchievedInput.getAttribute('class')).toContain('hidden'); + }); + }); + + it('should show when "yes, completed" is clicked', async () => { + const { fixture, getByLabelText } = await setup(); + + const yesCompletedRadioButton = getByLabelText('Yes, completed'); + + fireEvent.click(yesCompletedRadioButton); + fixture.detectChanges(); + + const yearAchievedInput = fixture.nativeElement.querySelector('div[id="certification-achieved"]'); + + expect(yearAchievedInput.getAttribute('class')).not.toContain('hidden'); + }); + }); + + describe('navigation', () => { + it('should navigate to apprenticeship-training page when submitting from flow', async () => { + const { component, fixture, routerSpy, getByText } = await setup(); + + const workerId = component.worker.uid; + const workplaceId = component.workplace.uid; + const radioBtn = fixture.nativeElement.querySelector('input[id="level2CareCertificate-yesStarted"]'); + const saveButton = getByText('Save and continue'); + + fireEvent.click(radioBtn); + fireEvent.click(saveButton); + fixture.detectChanges(); + + expect(getByText('Save and continue')).toBeTruthy(); + + expect(routerSpy).toHaveBeenCalledWith([ + '/workplace', + workplaceId, + 'staff-record', + workerId, + 'apprenticeship-training', + ]); + }); + + it('should navigate to apprenticeship-training page when skipping the question in the flow', async () => { + const { component, routerSpy, getByText } = await setup(); + + const workerId = component.worker.uid; + const workplaceId = component.workplace.uid; + + const link = getByText('Skip this question'); + fireEvent.click(link); + + expect(routerSpy).toHaveBeenCalledWith([ + '/workplace', + workplaceId, + 'staff-record', + workerId, + 'apprenticeship-training', + ]); + }); + + it('should navigate to staff-summary-page page when pressing view this staff record', async () => { + const { component, routerSpy, getByText } = await setup(); + + const workerId = component.worker.uid; + const workplaceId = component.workplace.uid; + + const link = getByText('View this staff record'); + fireEvent.click(link); + + expect(routerSpy).toHaveBeenCalledWith([ + '/workplace', + workplaceId, + 'staff-record', + workerId, + 'staff-record-summary', + ]); + }); + + it('should navigate to staff-summary-page page when pressing save and return', async () => { + const { component, fixture, routerSpy, getByText } = await setup(false); + + const workerId = component.worker.uid; + const workplaceId = component.workplace.uid; + const radioBtn = fixture.nativeElement.querySelector('input[id="level2CareCertificate-yesStarted"]'); + const saveButton = getByText('Save and return'); + + fireEvent.click(radioBtn); + fireEvent.click(saveButton); + fixture.detectChanges(); + + expect(routerSpy).toHaveBeenCalledWith([ + '/workplace', + workplaceId, + 'staff-record', + workerId, + 'staff-record-summary', + ]); + }); + + it('should navigate to staff-summary-page page when pressing cancel', async () => { + const { component, routerSpy, getByText } = await setup(false); + + const workerId = component.worker.uid; + const workplaceId = component.workplace.uid; + + const link = getByText('Cancel'); + fireEvent.click(link); + + expect(routerSpy).toHaveBeenCalledWith([ + '/workplace', + workplaceId, + 'staff-record', + workerId, + 'staff-record-summary', + ]); + }); + + it('should navigate to wdf staff-summary-page page when pressing save and return in wdf version of page', async () => { + const { component, router, fixture, routerSpy, getByText } = await setup(false); + spyOnProperty(router, 'url').and.returnValue('/wdf/staff-record'); + component.returnUrl = undefined; + component.ngOnInit(); + fixture.detectChanges(); + const workerId = component.worker.uid; + + const radioBtn = fixture.nativeElement.querySelector('input[id="level2CareCertificate-yesStarted"]'); + const saveButton = getByText('Save and return'); + + fireEvent.click(radioBtn); + fireEvent.click(saveButton); + fixture.detectChanges(); + + expect(routerSpy).toHaveBeenCalledWith(['/wdf', 'staff-record', workerId]); + }); + + it('should navigate to wdf staff-summary-page page when pressing cancel in wdf version of page', async () => { + const { component, router, fixture, routerSpy, getByText } = await setup(false); + spyOnProperty(router, 'url').and.returnValue('/wdf/staff-record'); + component.returnUrl = undefined; + component.ngOnInit(); + fixture.detectChanges(); + const workerId = component.worker.uid; + + const link = getByText('Cancel'); + fireEvent.click(link); + + expect(routerSpy).toHaveBeenCalledWith(['/wdf', 'staff-record', workerId]); + }); + }); + + describe('pre-fill', () => { + it('should only show radio button checked', async () => { + const workerFields = { level2CareCertificate: { value: 'No', year: null } }; + const { component, fixture } = await setup(false, workerFields); + + fixture.detectChanges(); + + const form = component.form; + const radioBtn = fixture.nativeElement.querySelector('input[id="level2CareCertificate-no"]'); + const yearAchievedInput = fixture.nativeElement.querySelector('div[id="certification-achieved"]'); + + expect(radioBtn.checked).toBeTruthy(); + expect(yearAchievedInput.getAttribute('class')).toContain('hidden'); + expect(form.value).toEqual({ level2CareCertificate: 'No', level2CareCertificateYearAchieved: null }); + }); + + it('should show radio and year input', async () => { + const workerFields = { level2CareCertificate: { value: 'Yes, completed', year: 2023 } }; + const { component, fixture } = await setup(false, workerFields); + + fixture.detectChanges(); + + const form = component.form; + const radioBtn = fixture.nativeElement.querySelector('input[id="level2CareCertificate-yesCompleted"]'); + const yearAchievedInput = fixture.nativeElement.querySelector('div[id="certification-achieved"]'); + + expect(radioBtn.checked).toBeTruthy(); + expect(yearAchievedInput.getAttribute('class')).not.toContain('hidden'); + expect(form.value).toEqual({ level2CareCertificate: 'Yes, completed', level2CareCertificateYearAchieved: 2023 }); + }); + }); + + describe('errors', () => { + it('should not show if "Yes, completed" is clicked but no year entered', async () => { + const { component, fixture, getByText, queryByText } = await setup(false); + + const form = component.form; + const radioBtn = fixture.nativeElement.querySelector('input[id="level2CareCertificate-yesCompleted"]'); + const saveButton = getByText('Save and return'); + + fireEvent.click(radioBtn); + fireEvent.click(saveButton); + fixture.detectChanges(); + + expect(form.valid).toBeTruthy(); + expect(queryByText('There is a problem')).toBeFalsy(); + }); + + it('should show if the entered year is in the future', async () => { + const { component, fixture, getByText, getAllByText } = await setup(false); + + const form = component.form; + const radioBtn = fixture.nativeElement.querySelector('input[id="level2CareCertificate-yesCompleted"]'); + const saveButton = getByText('Save and return'); + const expectedErrorMessage = 'Year achieved cannot be in the future'; + const nextYear = dayjs().year() + 1; + + form.get('level2CareCertificateYearAchieved').setValue(nextYear); + form.markAsDirty(); + fireEvent.click(radioBtn); + fireEvent.click(saveButton); + + expect(form.invalid).toBeTruthy(); + expect(getAllByText(expectedErrorMessage, { exact: false }).length).toBe(2); + }); + + it('should error if entered year is before the qualification was introduced', async () => { + const { component, fixture, getByText, getAllByText } = await setup(false); + + const form = component.form; + const radioBtn = fixture.nativeElement.querySelector('input[id="level2CareCertificate-yesCompleted"]'); + const saveButton = getByText('Save and return'); + + form.get('level2CareCertificateYearAchieved').setValue(2023); + form.markAsDirty(); + fireEvent.click(radioBtn); + fireEvent.click(saveButton); + + const expectedErrorMessage = 'Year achieved cannot be before 2024'; + + expect(form.invalid).toBeTruthy(); + expect(getAllByText(expectedErrorMessage, { exact: false }).length).toBe(2); + }); + }); +}); diff --git a/frontend/src/app/features/workers/level-2-adult-social-care-certificate/level-2-adult-social-care-certificate.component.ts b/frontend/src/app/features/workers/level-2-adult-social-care-certificate/level-2-adult-social-care-certificate.component.ts new file mode 100644 index 0000000000..10ef395367 --- /dev/null +++ b/frontend/src/app/features/workers/level-2-adult-social-care-certificate/level-2-adult-social-care-certificate.component.ts @@ -0,0 +1,93 @@ +import { Component } from '@angular/core'; +import { QuestionComponent } from '../question/question.component'; +import { UntypedFormBuilder, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BackLinkService } from '@core/services/backLink.service'; +import { ErrorSummaryService } from '@core/services/error-summary.service'; +import { WorkerService } from '@core/services/worker.service'; +import { EstablishmentService } from '@core/services/establishment.service'; +import dayjs from 'dayjs'; + +@Component({ + selector: 'app-level-2-adult-social-care-certificate', + templateUrl: './level-2-adult-social-care-certificate.component.html', +}) +export class Level2AdultSocialCareCertificateComponent extends QuestionComponent { + public answersAvailable = ['Yes, completed', 'Yes, started', 'No']; + + public section = 'Training and qualifications'; + + constructor( + protected formBuilder: UntypedFormBuilder, + protected router: Router, + protected route: ActivatedRoute, + protected backLinkService: BackLinkService, + protected errorSummaryService: ErrorSummaryService, + protected workerService: WorkerService, + protected establishmentService: EstablishmentService, + ) { + super(formBuilder, router, route, backLinkService, errorSummaryService, workerService, establishmentService); + + this.form = this.formBuilder.group({ + level2CareCertificate: null, + level2CareCertificateYearAchieved: [null, { validators: null, updateOn: 'submit' }], + }); + } + + init() { + this.next = this.getRoutePath('apprenticeship-training'); + + this.subscriptions.add( + this.form.get('level2CareCertificate').valueChanges.subscribe((value) => { + this.form.get('level2CareCertificateYearAchieved').clearValidators(); + + if (value === 'Yes, completed') { + this.form + .get('level2CareCertificateYearAchieved') + .setValidators([Validators.min(2024), Validators.max(dayjs().year())]); + } + this.form.get('level2CareCertificateYearAchieved').updateValueAndValidity(); + }), + ); + + if (this.worker.level2CareCertificate && this.worker.level2CareCertificate.value) { + this.form.patchValue({ + level2CareCertificate: this.worker.level2CareCertificate.value, + level2CareCertificateYearAchieved: this.worker.level2CareCertificate.year, + }); + } + } + + generateUpdateProps() { + const { level2CareCertificate, level2CareCertificateYearAchieved } = this.form.value; + + if (!level2CareCertificate) { + return null; + } + + return { + level2CareCertificate: { + value: level2CareCertificate, + year: level2CareCertificateYearAchieved, + }, + }; + } + + setupFormErrorsMap(): void { + this.formErrorsMap = [ + { + item: 'level2CareCertificateYearAchieved', + type: [ + { + name: 'min', + message: `Year achieved cannot be before 2024`, + }, + { + name: 'max', + message: `Year achieved cannot be in the future`, + }, + ], + }, + ]; + } +} diff --git a/frontend/src/app/features/workers/workers-routing.module.ts b/frontend/src/app/features/workers/workers-routing.module.ts index 2efdafa5da..56ea30f210 100644 --- a/frontend/src/app/features/workers/workers-routing.module.ts +++ b/frontend/src/app/features/workers/workers-routing.module.ts @@ -52,6 +52,7 @@ import { TotalStaffChangeComponent } from './total-staff-change/total-staff-chan import { WeeklyContractedHoursComponent } from './weekly-contracted-hours/weekly-contracted-hours.component'; import { YearArrivedUkComponent } from './year-arrived-uk/year-arrived-uk.component'; import { EmployedFromOutsideUkComponent } from './employed-from-outside-uk/employed-from-outside-uk.component'; +import { Level2AdultSocialCareCertificateComponent } from './level-2-adult-social-care-certificate/level-2-adult-social-care-certificate.component'; import { SelectTrainingCategoryComponent } from '@features/training-and-qualifications/add-edit-training/select-training-category/select-training-category.component'; import { TrainingCategoriesResolver } from '@core/resolvers/training-categories.resolver'; @@ -204,7 +205,7 @@ const routes: Routes = [ { path: 'inside-or-outside-of-uk', component: EmployedFromOutsideUkComponent, - data: { title: 'Inside or Outside UK'} + data: { title: 'Inside or Outside UK' }, }, { path: 'adult-social-care-started', @@ -241,6 +242,11 @@ const routes: Routes = [ component: CareCertificateComponent, data: { title: 'Care Certificate' }, }, + { + path: 'level-2-care-certificate', + component: Level2AdultSocialCareCertificateComponent, + data: { title: 'Level 2 Adult Social Care Certificate' }, + }, { path: 'apprenticeship-training', component: ApprenticeshipTrainingComponent, @@ -293,19 +299,19 @@ const routes: Routes = [ path: 'add-training', children: [ { - path:'', + path: '', component: SelectTrainingCategoryComponent, data: { title: 'Add Training' }, resolve: { trainingCategories: TrainingCategoriesResolver, - } + }, }, { path: 'details', component: AddEditTrainingComponent, data: { title: 'Add Training' }, - } - ] + }, + ], }, { path: 'training/:trainingRecordId', @@ -461,7 +467,7 @@ const routes: Routes = [ { path: 'inside-or-outside-of-uk', component: EmployedFromOutsideUkComponent, - data: { title: 'Inside or Outside UK'} + data: { title: 'Inside or Outside UK' }, }, { path: 'adult-social-care-started', @@ -498,6 +504,11 @@ const routes: Routes = [ component: CareCertificateComponent, data: { title: 'Care Certificate' }, }, + { + path: 'level-2-care-certificate', + component: Level2AdultSocialCareCertificateComponent, + data: { title: 'Level 2 Adult Social Care Certificate' }, + }, { path: 'apprenticeship-training', component: ApprenticeshipTrainingComponent, @@ -550,19 +561,19 @@ const routes: Routes = [ path: 'add-training', children: [ { - path:'', + path: '', component: SelectTrainingCategoryComponent, data: { title: 'Add Training' }, resolve: { trainingCategories: TrainingCategoriesResolver, - } + }, }, { path: 'details', component: AddEditTrainingComponent, data: { title: 'Add Training' }, - } - ] + }, + ], }, { path: 'training/:trainingRecordId', diff --git a/frontend/src/app/features/workers/workers.module.ts b/frontend/src/app/features/workers/workers.module.ts index b639addad6..e77e3b0b71 100644 --- a/frontend/src/app/features/workers/workers.module.ts +++ b/frontend/src/app/features/workers/workers.module.ts @@ -65,6 +65,7 @@ import { WeeklyContractedHoursComponent } from './weekly-contracted-hours/weekly import { WorkersRoutingModule } from './workers-routing.module'; import { YearArrivedUkComponent } from './year-arrived-uk/year-arrived-uk.component'; import { EmployedFromOutsideUkComponent } from './employed-from-outside-uk/employed-from-outside-uk.component'; +import { Level2AdultSocialCareCertificateComponent } from './level-2-adult-social-care-certificate/level-2-adult-social-care-certificate.component'; import { TrainingCategoriesResolver } from '@core/resolvers/training-categories.resolver'; @NgModule({ @@ -120,6 +121,7 @@ import { TrainingCategoriesResolver } from '@core/resolvers/training-categories. DownloadPdfTrainingAndQualificationComponent, HealthAndCareVisaComponent, EmployedFromOutsideUkComponent, + Level2AdultSocialCareCertificateComponent, ], providers: [ DialogService, diff --git a/frontend/src/app/shared/components/staff-record-summary/qualifications-and-training/qualifications-and-training.component.html b/frontend/src/app/shared/components/staff-record-summary/qualifications-and-training/qualifications-and-training.component.html index a864317e5f..cb4c0834ba 100644 --- a/frontend/src/app/shared/components/staff-record-summary/qualifications-and-training/qualifications-and-training.component.html +++ b/frontend/src/app/shared/components/staff-record-summary/qualifications-and-training/qualifications-and-training.component.html @@ -19,7 +19,7 @@

Training and qualifications

!worker.wdf?.careCertificate.updatedSinceEffectiveDate && worker.careCertificate !== 'Yes, completed' " - [changeLink]="getRoutePath('care-certificate',wdfView)" + [changeLink]="getRoutePath('care-certificate', wdfView)" (fieldConfirmation)="this.confirmField('careCertificate')" (setReturnClicked)="this.setReturn()" [workerUid]="worker.uid" @@ -43,6 +43,22 @@

Training and qualifications

+
+
Level 2 Adult Social Care Certificate
+ +
+ {{ worker.level2CareCertificate?.value || '-' }} +
+
+ +
+
+
Apprenticeship training
@@ -76,7 +92,7 @@

Training and qualifications

!worker.wdf?.qualificationInSocialCare.updatedSinceEffectiveDate && worker.qualificationInSocialCare !== 'Yes' " - [changeLink]="getRoutePath('social-care-qualification',wdfView)" + [changeLink]="getRoutePath('social-care-qualification', wdfView)" (fieldConfirmation)="this.confirmField('qualificationInSocialCare')" (setReturnClicked)="this.setReturn()" [workerUid]="worker.uid" @@ -117,7 +133,7 @@

Training and qualifications

worker.wdf?.socialCareQualification.isEligible === 'Yes' && !worker.wdf?.socialCareQualification.updatedSinceEffectiveDate " - [changeLink]="getRoutePath('social-care-qualification-level',wdfView)" + [changeLink]="getRoutePath('social-care-qualification-level', wdfView)" (fieldConfirmation)=" this.confirmField('socialCareQualification'); this.confirmField('qualificationInSocialCare') " @@ -160,7 +176,7 @@

Training and qualifications

!worker.wdf?.otherQualification.updatedSinceEffectiveDate && worker.otherQualification !== 'Yes' " - [changeLink]="getRoutePath('other-qualifications',wdfView)" + [changeLink]="getRoutePath('other-qualifications', wdfView)" (fieldConfirmation)="this.confirmField('otherQualification')" (setReturnClicked)="this.setReturn()" [workerUid]="worker.uid" @@ -193,7 +209,7 @@

Training and qualifications

worker.wdf?.highestQualification.isEligible === 'Yes' && !worker.wdf?.highestQualification.updatedSinceEffectiveDate " - [changeLink]="getRoutePath('other-qualifications-level',wdfView)" + [changeLink]="getRoutePath('other-qualifications-level', wdfView)" (fieldConfirmation)="this.confirmField('highestQualification'); this.confirmField('otherQualification')" (setReturnClicked)="this.setReturn()" [workerUid]="worker.uid" diff --git a/frontend/src/app/shared/components/staff-record-summary/qualifications-and-training/qualifications-and-training.component.spec.ts b/frontend/src/app/shared/components/staff-record-summary/qualifications-and-training/qualifications-and-training.component.spec.ts new file mode 100644 index 0000000000..62ffe6b2f5 --- /dev/null +++ b/frontend/src/app/shared/components/staff-record-summary/qualifications-and-training/qualifications-and-training.component.spec.ts @@ -0,0 +1,156 @@ +import { HttpClient } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterModule } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Establishment } from '@core/model/establishment.model'; +import { Worker } from '@core/model/worker.model'; +import { PermissionsService } from '@core/services/permissions/permissions.service'; +import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService'; +import { MockPermissionsService } from '@core/test-utils/MockPermissionsService'; +import { workerWithWdf } from '@core/test-utils/MockWorkerService'; +import { SummaryRecordChangeComponent } from '@shared/components/summary-record-change/summary-record-change.component'; +import { SharedModule } from '@shared/shared.module'; +import { render, within } from '@testing-library/angular'; + +import { QualificationsAndTrainingComponent } from './qualifications-and-training.component'; +import { InternationalRecruitmentService } from '@core/services/international-recruitment.service'; + +describe('QualificationsAndTrainingComponent', () => { + async function setup(isWdf = false, canEditWorker = true) { + const { fixture, getByText } = await render(QualificationsAndTrainingComponent, { + imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule], + declarations: [SummaryRecordChangeComponent], + providers: [ + InternationalRecruitmentService, + { + provide: PermissionsService, + useFactory: MockPermissionsService.factory(canEditWorker ? ['canEditWorker'] : []), + deps: [HttpClient], + }, + ], + componentProperties: { + canEditWorker: canEditWorker, + workplace: establishmentBuilder() as Establishment, + worker: workerWithWdf() as Worker, + wdfView: isWdf, + }, + }); + + const component = fixture.componentInstance; + + return { + component, + fixture, + getByText, + }; + } + + it('should render a QualificationsAndTrainingComponent', async () => { + const { component } = await setup(); + expect(component).toBeTruthy(); + }); + + describe('care certificate', () => { + it('should render Add link with the staff-record-summary/care-certificate url when care certificate is not answered', async () => { + const { fixture, component, getByText } = await setup(); + + component.worker.careCertificate = null; + fixture.detectChanges(); + + const careCertificateSection = getByText('Care Certificate').parentElement; + const addLink = within(careCertificateSection).getByText('Add'); + + expect(addLink.getAttribute('href')).toBe( + `/workplace/${component.workplace.uid}/staff-record/${component.worker.uid}/staff-record-summary/care-certificate`, + ); + }); + + it('should render Change link with the staff-record-summary/care-certificate url when care certificate is already answered', async () => { + const { fixture, component, getByText } = await setup(); + + component.worker.careCertificate = 'Yes, completed'; + fixture.detectChanges(); + + const careCertificateSection = getByText('Care Certificate').parentElement; + const currentAnswer = within(careCertificateSection).getByText('Yes, completed'); + const changeLink = within(careCertificateSection).getByText('Change'); + + expect(currentAnswer).toBeTruthy(); + expect(changeLink.getAttribute('href')).toBe( + `/workplace/${component.workplace.uid}/staff-record/${component.worker.uid}/staff-record-summary/care-certificate`, + ); + }); + }); + + describe('level 2 care certificate', () => { + it('should render Add link with the staff-record-summary/level-2-adult-social-care-certificate url when level 2 care certificate is not answered', async () => { + const { fixture, component, getByText } = await setup(); + + component.worker.level2CareCertificate = null; + fixture.detectChanges(); + + const level2CareCertificateSection = getByText('Level 2 Adult Social Care Certificate').parentElement; + const addLink = within(level2CareCertificateSection).getByText('Add'); + + expect(addLink.getAttribute('href')).toBe( + `/workplace/${component.workplace.uid}/staff-record/${component.worker.uid}/staff-record-summary/level-2-care-certificate`, + ); + }); + + it('should render Change link with the staff-record-summary/level-2-adult-social-care-certificate url when care certificate is already answered', async () => { + const { fixture, component, getByText } = await setup(); + + component.worker.level2CareCertificate.value = 'Yes, started'; + fixture.detectChanges(); + + const level2CareCertificateSection = getByText('Level 2 Adult Social Care Certificate').parentElement; + const currentAnswer = within(level2CareCertificateSection).getByText('Yes, started'); + const changeLink = within(level2CareCertificateSection).getByText('Change'); + + expect(currentAnswer).toBeTruthy(); + expect(changeLink.getAttribute('href')).toBe( + `/workplace/${component.workplace.uid}/staff-record/${component.worker.uid}/staff-record-summary/level-2-care-certificate`, + ); + }); + + it('should not render Add or Change link when canEditWorker is false', async () => { + const { fixture, component, getByText } = await setup(false, false); + + component.worker.level2CareCertificate.value = 'Yes, started'; + fixture.detectChanges(); + + const level2CareCertificateSection = getByText('Level 2 Adult Social Care Certificate').parentElement; + const addLink = within(level2CareCertificateSection).queryByText('Add'); + const changeLink = within(level2CareCertificateSection).queryByText('Change'); + + expect(addLink).toBeFalsy(); + expect(changeLink).toBeFalsy(); + }); + + describe('wdf version', () => { + it('should render Add link to the wdf question page in wdf version of summary page when level 2 care certificate is not answered', async () => { + const { fixture, component, getByText } = await setup(true); + + component.worker.level2CareCertificate = null; + fixture.detectChanges(); + + const level2CareCertificateSection = getByText('Level 2 Adult Social Care Certificate').parentElement; + const addLink = within(level2CareCertificateSection).getByText('Add'); + + expect(addLink.getAttribute('href')).toBe(`/wdf/staff-record/${component.worker.uid}/level-2-care-certificate`); + }); + + it('should render Change link to the wdf question page in wdf version of summary page when level 2 care certificate is answered', async () => { + const { fixture, component, getByText } = await setup(true); + + component.worker.level2CareCertificate.value = 'Yes, started'; + fixture.detectChanges(); + + const level2CareCertificateSection = getByText('Level 2 Adult Social Care Certificate').parentElement; + const addLink = within(level2CareCertificateSection).getByText('Change'); + + expect(addLink.getAttribute('href')).toBe(`/wdf/staff-record/${component.worker.uid}/level-2-care-certificate`); + }); + }); + }); +}); diff --git a/frontend/src/assets/scss/components/_inset-text.scss b/frontend/src/assets/scss/components/_inset-text.scss index 7ddd43752e..b6f9a42845 100644 --- a/frontend/src/assets/scss/components/_inset-text.scss +++ b/frontend/src/assets/scss/components/_inset-text.scss @@ -18,4 +18,9 @@ border-color: govuk-colour('blue'); background-color: rgba(govuk-colour('blue'), 0.15); } + + &.thin-border { + border-width: 5px; + padding-left: 20px; + } } diff --git a/frontend/src/assets/scss/partials/_radios.scss b/frontend/src/assets/scss/partials/_radios.scss index 1fa574f5f7..81b56897a2 100644 --- a/frontend/src/assets/scss/partials/_radios.scss +++ b/frontend/src/assets/scss/partials/_radios.scss @@ -21,3 +21,7 @@ vertical-align: middle; padding: 5px 0px 5px 10px; } + +.govuk-radios__conditional { + border-left: 5px solid #b1b4b6; +} diff --git a/lambdas/bulkUpload/classes/workerCSVValidator.js b/lambdas/bulkUpload/classes/workerCSVValidator.js index bdb4ed832b..2ed2063714 100644 --- a/lambdas/bulkUpload/classes/workerCSVValidator.js +++ b/lambdas/bulkUpload/classes/workerCSVValidator.js @@ -34,6 +34,7 @@ class WorkerCsvValidator { this._disabled = null; this._careCert = null; + this._level2CareCert = null; this._recSource = null; this._startDate = null; @@ -358,6 +359,30 @@ class WorkerCsvValidator { return 5580; } + static get L2CARECERT_WARNING() { + return 5590; + } + + static get L2CARECERT_WARNING_IGNORE_YEAR_FOR_OPTION_2() { + return 5600; + } + + static get L2CARECERT_WARNING_IGNORE_YEAR_FOR_OPTION_3() { + return 5610; + } + + static get L2CARECERT_WARNING_YEAR_BEFORE_2024() { + return 5620; + } + + static get L2CARECERT_WARNING_YEAR_IN_FUTURE() { + return 5630; + } + + static get L2CARECERT_WARNING_YEAR_INVALID() { + return 5640; + } + get lineNumber() { return this._lineNumber; } @@ -438,6 +463,10 @@ class WorkerCsvValidator { return this._careCert; } + get level2CareCert() { + return this._level2CareCert; + } + get recSource() { return this._recSource; } @@ -512,14 +541,16 @@ class WorkerCsvValidator { return mappings[value] || ''; } - _generateWarning(warning, columnName) { - const warningType = `${columnName}_WARNING`; + _generateWarning(warning, columnName, warnType = null) { + if (!warnType) { + warnType = `${columnName}_WARNING`; + } return { worker: this._currentLine.UNIQUEWORKERID, name: this._currentLine.LOCALESTID, lineNumber: this._lineNumber, - warnCode: WorkerCsvValidator[warningType], - warnType: warningType, + warnCode: WorkerCsvValidator[warnType], + warnType: warnType, warning, source: this._currentLine[columnName], column: columnName, @@ -1190,7 +1221,7 @@ class WorkerCsvValidator { const myCareCert = parseInt(this._currentLine.CARECERT, 10); if (this._currentLine.CARECERT && this._currentLine.CARECERT.length > 0) { - if (isNaN(myCareCert) || !careCertValues.includes(parseInt(myCareCert, 10))) { + if (isNaN(myCareCert) || !careCertValues.includes(myCareCert)) { this._validationErrors.push({ worker: this._currentLine.UNIQUEWORKERID, name: this._currentLine.LOCALESTID, @@ -1221,6 +1252,81 @@ class WorkerCsvValidator { } } + _validateLevel2CareCert() { + const allowedLevel2CareCertValues = [1, 2, 3]; + + const inputIsEmpty = !(this._currentLine.L2CARECERT && this._currentLine.L2CARECERT.length > 0); + + if (inputIsEmpty) { + return true; + } + + const [valueString, yearString] = this._currentLine.L2CARECERT.split(';'); + const myLevel2CareCertValue = parseInt(valueString, 10); + + if (!allowedLevel2CareCertValues.includes(myLevel2CareCertValue)) { + const warning = this._generateWarning( + 'The code you have entered for L2CARECERT is incorrect and will be ignored', + 'L2CARECERT', + ); + this._validationErrors.push(warning); + return false; + } + + if (myLevel2CareCertValue === 1) { + if (yearString === '' || !yearString) { + this._level2CareCert = { value: 'Yes, completed', year: null }; + return true; + } + + return this._handleLevel2CareCertCompleteWithAchievedYear(yearString); + } + + if ([2, 3].includes(myLevel2CareCertValue)) { + if (yearString) { + const warning = this._generateWarning( + `Option ${myLevel2CareCertValue} for L2CARECERT cannot have year achieved and will be ignored`, + 'L2CARECERT', + `L2CARECERT_WARNING_IGNORE_YEAR_FOR_OPTION_${myLevel2CareCertValue}`, + ); + this._validationErrors.push(warning); + return false; + } + + this._level2CareCert = { value: myLevel2CareCertValue === 2 ? 'Yes, started' : 'No', year: null }; + return true; + } + } + + _handleLevel2CareCertCompleteWithAchievedYear(yearString) { + const yearLevel2CareCertificateIntroduced = 2024; + const thisYear = new Date().getFullYear(); + const parsedYear = parseInt(yearString, 10); + let warningMessage; + let warnType; + + if (isNaN(parsedYear)) { + warningMessage = 'The year achieved for L2CARECERT is invalid. The year value will be ignored'; + warnType = 'L2CARECERT_WARNING_YEAR_INVALID'; + } else if (parsedYear < yearLevel2CareCertificateIntroduced) { + warningMessage = 'The year achieved for L2CARECERT cannot be before 2024. The year value will be ignored'; + warnType = 'L2CARECERT_WARNING_YEAR_BEFORE_2024'; + } else if (parsedYear > thisYear) { + warningMessage = 'The year achieved for L2CARECERT cannot be in the future. The year value will be ignored'; + warnType = 'L2CARECERT_WARNING_YEAR_IN_FUTURE'; + } + + if (warningMessage) { + this._level2CareCert = { value: 'Yes, completed', year: null }; + const warning = this._generateWarning(warningMessage, 'L2CARECERT', warnType); + this._validationErrors.push(warning); + return false; + } else { + this._level2CareCert = { value: 'Yes, completed', year: parsedYear }; + return true; + } + } + _validateRecSource() { const myRecSource = parseInt(this._currentLine.RECSOURCE, 10); @@ -2775,6 +2881,7 @@ class WorkerCsvValidator { status = !this._validateEmployedFromOutsideUk() ? false : status; status = !this._validateDisabled() ? false : status; status = !this._validateCareCert() ? false : status; + status = !this._validateLevel2CareCert() ? false : status; status = !this._validateRecSource() ? false : status; status = !this._validateStartDate() ? false : status; status = !this._validateStartInsect() ? false : status; @@ -2858,6 +2965,7 @@ class WorkerCsvValidator { value: this._careCert, } : undefined, + level2CareCertificate: this._level2CareCert?.value ? this._level2CareCert : undefined, recruitmentSource: this._recSource ? this._recSource : undefined, startDate: this._startDate ? this._startDate.format('DD/MM/YYYY') : undefined, startedInSector: this._startInsect ? this._startInsect : undefined, @@ -2940,6 +3048,7 @@ class WorkerCsvValidator { employedFromOutsideUk: this._employedFromOutsideUk ? this._employedFromOutsideUk : undefined, disability: this._disabled ? this._disabled : undefined, careCertificate: this._careCert ? this._careCert : undefined, + level2CareCertificate: this._level2CareCert?.value ? this._level2CareCert : undefined, apprenticeshipTraining: this._apprentice ? this._apprentice : undefined, zeroHoursContract: this._zeroHourContract ? this._zeroHourContract : undefined, registeredNurse: this._registeredNurse ? this._registeredNurse : undefined, diff --git a/lambdas/bulkUpload/test/unit/classes/workerCSVValidator.spec.js b/lambdas/bulkUpload/test/unit/classes/workerCSVValidator.spec.js index 48f0a34b55..5909071af8 100644 --- a/lambdas/bulkUpload/test/unit/classes/workerCSVValidator.spec.js +++ b/lambdas/bulkUpload/test/unit/classes/workerCSVValidator.spec.js @@ -1,5 +1,7 @@ const expect = require('chai').expect; +const { before, after } = require('mocha'); const moment = require('moment'); +const sinon = require('sinon'); const WorkerCsvValidator = require('../../../classes/workerCSVValidator').WorkerCsvValidator; const { build } = require('@jackfranklin/test-data-bot'); const mappings = require('../../../../../backend/reference/BUDIMappings').mappings; @@ -11,6 +13,7 @@ const buildWorkerCsv = build('WorkerCSV', { AVGHOURS: '', BRITISHCITIZENSHIP: '', CARECERT: '3', + L2CARECERT: '3', CONTHOURS: '23', COUNTRYOFBIRTH: '826', DAYSSICK: '1', @@ -1222,5 +1225,209 @@ describe('/lambdas/bulkUpload/classes/workerCSVValidator', async () => { expect(validator._validationErrors.length).to.equal(1); }); }); + + describe('_validateLevel2CareCert()', () => { + let clock; + before(() => { + // stub current year as 2025 to test behavior related to year in future + clock = sinon.useFakeTimers(new Date(2025, 1, 1)); + }); + after(() => { + clock.restore(); + }); + + describe('Valid inputs', () => { + [ + { l2CareCert: '1;', mapping: { value: 'Yes, completed', year: null } }, + { l2CareCert: '1;2024', mapping: { value: 'Yes, completed', year: 2024 } }, + { l2CareCert: '1;2025', mapping: { value: 'Yes, completed', year: 2025 } }, + { l2CareCert: '2;', mapping: { value: 'Yes, started', year: null } }, + { l2CareCert: '3;', mapping: { value: 'No', year: null } }, + { l2CareCert: '1', mapping: { value: 'Yes, completed', year: null } }, + { l2CareCert: '2', mapping: { value: 'Yes, started', year: null } }, + { l2CareCert: '3', mapping: { value: 'No', year: null } }, + ].forEach((answer) => { + it(`should not add warning when valid (${answer.l2CareCert}) level 2 care certificate value provided`, async () => { + const worker = buildWorkerCsv({ + overrides: { + STATUS: 'NEW', + L2CARECERT: answer.l2CareCert, + }, + }); + const validator = new WorkerCsvValidator(worker, 2, null, mappings); + + validator.validate(); + validator.transform(); + + expect(validator._validationErrors).to.deep.equal([]); + expect(validator._validationErrors.length).to.equal(0); + }); + + it(`should set level 2 care certificate value field with database mapping (${JSON.stringify( + answer.mapping, + )}) when valid (${answer.l2CareCert}) L2CARECERT provided`, () => { + const worker = buildWorkerCsv({ + overrides: { + STATUS: 'NEW', + L2CARECERT: answer.l2CareCert, + }, + }); + const validator = new WorkerCsvValidator(worker, 2, null, mappings); + + validator.validate(); + validator.transform(); + + expect(validator._level2CareCert).to.deep.equal(answer.mapping); + }); + }); + }); + + describe('Partially accepted inputs', () => { + const warningMessages = { + yearBefore2024: 'The year achieved for L2CARECERT cannot be before 2024. The year value will be ignored', + yearInFuture: 'The year achieved for L2CARECERT cannot be in the future. The year value will be ignored', + otherCase: 'The year achieved for L2CARECERT is invalid. The year value will be ignored', + }; + const warnTypes = { + yearBefore2024: 'L2CARECERT_WARNING_YEAR_BEFORE_2024', + yearInFuture: 'L2CARECERT_WARNING_YEAR_IN_FUTURE', + otherCase: 'L2CARECERT_WARNING_YEAR_INVALID', + }; + + const testCasesWithInvalidYears = [ + { + l2CareCertInput: '1;2000', + warningMessage: warningMessages.yearBefore2024, + warnType: warnTypes.yearBefore2024, + }, + { + l2CareCertInput: '1;2023', + warningMessage: warningMessages.yearBefore2024, + warnType: warnTypes.yearBefore2024, + }, + { l2CareCertInput: '1;2099', warningMessage: warningMessages.yearInFuture, warnType: warnTypes.yearInFuture }, + { l2CareCertInput: '1;2026', warningMessage: warningMessages.yearInFuture, warnType: warnTypes.yearInFuture }, + { l2CareCertInput: '1;abc', warningMessage: warningMessages.otherCase, warnType: warnTypes.otherCase }, + ]; + testCasesWithInvalidYears.forEach(({ l2CareCertInput, warningMessage, warnType }) => { + it(`given a valid value but incorrect year: (${l2CareCertInput}), ignore the year`, () => { + const expected = { value: 'Yes, completed', year: null }; + + const worker = buildWorkerCsv({ + overrides: { + STATUS: 'NEW', + L2CARECERT: l2CareCertInput, + }, + }); + + const validator = new WorkerCsvValidator(worker, 2, null, mappings); + + validator.validate(); + validator.transform(); + + expect(validator._level2CareCert).to.deep.equal(expected); + }); + + it('add a warning message about year being invalid', () => { + const worker = buildWorkerCsv({ + overrides: { + STATUS: 'NEW', + L2CARECERT: l2CareCertInput, + }, + }); + + const expectedParsedValue = { value: 'Yes, completed', year: null }; + + const expectedWarning = { + column: 'L2CARECERT', + lineNumber: 2, + name: 'MARMA', + source: l2CareCertInput, + warnCode: WorkerCsvValidator[warnType], + warnType: warnType, + warning: warningMessage, + worker: '3', + }; + + const validator = new WorkerCsvValidator(worker, 2, null, mappings); + + validator.validate(); + validator.transform(); + + expect(validator._validationErrors).to.deep.equal([expectedWarning]); + expect(validator._validationErrors.length).to.equal(1); + expect(validator._level2CareCert).to.deep.equal(expectedParsedValue); + }); + }); + }); + + describe('Invalid inputs', () => { + const invalidInputs = ['12345', '12345;2024', 'abc', 'abc;2024']; + invalidInputs.forEach((invalidLevel2CareCertInput) => { + it(`should add warning when the L2CARECERT value is invalid: (${invalidLevel2CareCertInput})`, async () => { + const worker = buildWorkerCsv({ + overrides: { + STATUS: 'NEW', + L2CARECERT: invalidLevel2CareCertInput, + }, + }); + const expectedWarning = { + column: 'L2CARECERT', + lineNumber: 2, + name: 'MARMA', + source: invalidLevel2CareCertInput, + warnCode: WorkerCsvValidator.L2CARECERT_WARNING, + warnType: 'L2CARECERT_WARNING', + warning: 'The code you have entered for L2CARECERT is incorrect and will be ignored', + worker: '3', + }; + + const validator = new WorkerCsvValidator(worker, 2, null, mappings); + + validator.validate(); + validator.transform(); + + expect(validator._validationErrors).to.deep.equal([expectedWarning]); + expect(validator._validationErrors.length).to.equal(1); + expect(validator._level2CareCert).to.equal(null); + }); + }); + + const invalidInputsWithYear = ['2;2024', '3;2024', '2;2000', '3;2000']; + + invalidInputsWithYear.forEach((invalidLevel2CareCertInput) => { + it(`should add warning and ignore the whole input when option 2 or 3 are given with year achieved: (${invalidLevel2CareCertInput})`, () => { + const optionChosen = invalidLevel2CareCertInput[0]; + + const worker = buildWorkerCsv({ + overrides: { + STATUS: 'NEW', + L2CARECERT: invalidLevel2CareCertInput, + }, + }); + const expectedWarnType = `L2CARECERT_WARNING_IGNORE_YEAR_FOR_OPTION_${optionChosen}`; + const expectedWarning = { + column: 'L2CARECERT', + lineNumber: 2, + name: 'MARMA', + source: invalidLevel2CareCertInput, + warnCode: WorkerCsvValidator[expectedWarnType], + warnType: expectedWarnType, + warning: `Option ${optionChosen} for L2CARECERT cannot have year achieved and will be ignored`, + worker: '3', + }; + + const validator = new WorkerCsvValidator(worker, 2, null, mappings); + + validator.validate(); + validator.transform(); + + expect(validator._validationErrors).to.deep.equal([expectedWarning]); + expect(validator._validationErrors.length).to.equal(1); + expect(validator._level2CareCert).to.equal(null); + }); + }); + }); + }); }); });