Skip to content

Commit

Permalink
Merge pull request #6413 from NMDSdevopsServiceAdm/spike/poc-for-bu-t…
Browse files Browse the repository at this point in the history
…ransfer-staff-record-column

Bulk upload - Transfer staff record
  • Loading branch information
duncanc19 authored Nov 28, 2024
2 parents 1347b67 + 03d1d29 commit 5191be9
Show file tree
Hide file tree
Showing 18 changed files with 969 additions and 212 deletions.
201 changes: 184 additions & 17 deletions backend/server/models/BulkImport/csv/crossValidate.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { chain } = require('lodash');
const models = require('../../../models');

const MAIN_JOB_ROLE_ERROR = () => 1280;
const { addCrossValidateError, MAIN_JOB_ERRORS, TRANSFER_STAFF_RECORD_ERRORS } = require('./crossValidateErrors');

const crossValidate = async (csvWorkerSchemaErrors, myEstablishments, JSONWorker) => {
if (workerNotChanged(JSONWorker)) {
Expand All @@ -14,24 +14,23 @@ const crossValidate = async (csvWorkerSchemaErrors, myEstablishments, JSONWorker

const _crossValidateMainJobRole = (csvWorkerSchemaErrors, isCqcRegulated, JSONWorker) => {
if (!isCqcRegulated && JSONWorker.mainJobRoleId === 4) {
csvWorkerSchemaErrors.unshift({
worker: JSONWorker.uniqueWorkerId,
name: JSONWorker.localId,
lineNumber: JSONWorker.lineNumber,
errCode: MAIN_JOB_ROLE_ERROR(),
errType: 'MAIN_JOB_ROLE_ERROR',
source: JSONWorker.mainJobRoleId,
column: 'MAINJOBROLE',
error:
'Workers MAINJOBROLE is Registered Manager but you are not providing a CQC regulated service. Please change to another Job Role',
});
addCrossValidateError(
csvWorkerSchemaErrors,
MAIN_JOB_ERRORS.RegisteredManagerWithoutCqcRegulatedService,
JSONWorker,
);
}
};

const _isCQCRegulated = async (myEstablishments, JSONWorker) => {
const workerEstablishment = myEstablishments.find(
(establishment) => JSONWorker.establishmentKey === establishment.key,
);
let workerEstablishmentKey;
if (isMovingToNewWorkplace(JSONWorker)) {
workerEstablishmentKey = JSONWorker.transferStaffRecord.replace(/\s/g, '');
} else {
workerEstablishmentKey = JSONWorker.establishmentKey;
}

const workerEstablishment = myEstablishments.find((establishment) => workerEstablishmentKey === establishment.key);

if (workerEstablishment) {
switch (workerEstablishment.status) {
Expand All @@ -49,13 +48,181 @@ const _isCQCRegulated = async (myEstablishments, JSONWorker) => {

const _checkEstablishmentRegulatedInDatabase = async (establishmentId) => {
const establishment = await models.establishment.findbyId(establishmentId);
return establishment.isRegulated;
return establishment?.isRegulated;
};

const workerNotChanged = (JSONWorker) => !['NEW', 'UPDATE'].includes(JSONWorker.status);

const crossValidateTransferStaffRecord = async (
csvWorkerSchemaErrors,
myAPIEstablishments,
myEstablishments,
myJSONWorkers,
) => {
const relatedEstablishmentIds = myEstablishments.map((establishment) => establishment.id);

const allMovingWorkers = myJSONWorkers.filter(isMovingToNewWorkplace);
const allNewWorkers = myJSONWorkers.filter((worker) => worker.status === 'NEW');
const allOtherWorkers = myJSONWorkers.filter((worker) => !isMovingToNewWorkplace(worker) && worker.status !== 'NEW');

const newWorkerWithDuplicateIdErrorAdded = _crossValidateWorkersWithDuplicateRefsMovingToWorkplace(
csvWorkerSchemaErrors,
allMovingWorkers,
allNewWorkers,
TRANSFER_STAFF_RECORD_ERRORS.SameRefsMovingToWorkplace,
);

if (newWorkerWithDuplicateIdErrorAdded) return;

const existingWorkerWithDuplicateIdErrorAdded = _crossValidateWorkersWithDuplicateRefsMovingToWorkplace(
csvWorkerSchemaErrors,
allMovingWorkers,
allOtherWorkers,
TRANSFER_STAFF_RECORD_ERRORS.SameLocalIdExistInNewWorkplace,
);

if (existingWorkerWithDuplicateIdErrorAdded) return;

for (const JSONWorker of allMovingWorkers) {
const newWorkplaceId = await _validateTransferIsPossible(
csvWorkerSchemaErrors,
relatedEstablishmentIds,
JSONWorker,
);

if (newWorkplaceId) {
_addNewWorkplaceIdToWorkerEntity(myAPIEstablishments, JSONWorker, newWorkplaceId);
}
}
};

const isMovingToNewWorkplace = (JSONWorker) => {
return JSONWorker.status === 'UPDATE' && JSONWorker.transferStaffRecord;
};

const _validateTransferIsPossible = async (csvWorkerSchemaErrors, relatedEstablishmentIds, JSONWorker) => {
const newWorkplaceLocalRef = JSONWorker.transferStaffRecord;
const newWorkplaceId = await _getNewWorkplaceId(newWorkplaceLocalRef, relatedEstablishmentIds);

if (newWorkplaceId === null) {
addCrossValidateError(csvWorkerSchemaErrors, TRANSFER_STAFF_RECORD_ERRORS.NewWorkplaceNotFound, JSONWorker);
return;
}

const workerReferenceToLookup = JSONWorker.changeUniqueWorker
? JSONWorker.changeUniqueWorker
: JSONWorker.uniqueWorkerId;

// if worker with duplicated reference found in database but not in csv file,
// changes to unique worker ID are applied before deleting workers not in file,
// which would cause bulk upload to break
// the code below prevents this issue

const uniqueWorkerIdFoundInWorkplaceInDatabase = await models.worker.findOneWithConflictingLocalRef(
newWorkplaceId,
workerReferenceToLookup,
);

if (uniqueWorkerIdFoundInWorkplaceInDatabase) {
addCrossValidateError(
csvWorkerSchemaErrors,
TRANSFER_STAFF_RECORD_ERRORS.SameLocalIdExistInNewWorkplace,
JSONWorker,
);
return;
}

if (_workerPassedAllValidations(csvWorkerSchemaErrors, JSONWorker)) {
return newWorkplaceId;
}
};

const _getNewWorkplaceId = async (newWorkplaceLocalRef, relatedEstablishmentIds) => {
const newWorkplaceFound = await models.establishment.findOne({
where: {
LocalIdentifierValue: newWorkplaceLocalRef,
id: relatedEstablishmentIds,
},
});
if (newWorkplaceFound) {
return newWorkplaceFound.id;
}

return null;
};

const _workerPassedAllValidations = (csvWorkerSchemaErrors, JSONWorker) => {
const errorForThisWorker = csvWorkerSchemaErrors.find(
(error) => error?.lineNumber === JSONWorker.lineNumber && error?.errType === 'TRANSFERSTAFFRECORD_ERROR',
);

return !errorForThisWorker;
};

const _addNewWorkplaceIdToWorkerEntity = (myAPIEstablishments, JSONWorker, newWorkplaceId) => {
const oldWorkplaceKey = JSONWorker.localId.replace(/\s/g, '');
const workerEntityKey = JSONWorker.uniqueWorkerId.replace(/\s/g, '');

const workerEntity = myAPIEstablishments[oldWorkplaceKey].theWorker(workerEntityKey);

if (workerEntity) {
workerEntity._newWorkplaceId = newWorkplaceId;
}
};

const _crossValidateWorkersWithDuplicateRefsMovingToWorkplace = (
csvWorkerSchemaErrors,
allMovingWorkers,
otherWorkers,
errorType,
) => {
const workplacesDict = _buildWorkplaceDictWithOtherWorkers(otherWorkers);

let errorAdded = false;

for (const JSONWorker of allMovingWorkers) {
const newWorkplaceRef = JSONWorker.transferStaffRecord;

const workerRef = JSONWorker.changeUniqueWorker
? JSONWorker.changeUniqueWorker.replace(/\s/g, '')
: JSONWorker.uniqueWorkerId.replace(/\s/g, '');

if (!workplacesDict[newWorkplaceRef]) {
workplacesDict[newWorkplaceRef] = new Set([workerRef]);
continue;
}

if (!workplacesDict[newWorkplaceRef].has(workerRef)) {
workplacesDict[newWorkplaceRef].add(workerRef);
continue;
}
// worker's ID exists in workplace in file
addCrossValidateError(csvWorkerSchemaErrors, errorType, JSONWorker);

errorAdded = true;
}

return errorAdded;
};

const _buildWorkplaceDictWithOtherWorkers = (otherWorkers) => {
return chain(otherWorkers)
.groupBy('localId') // workplace ref
.mapValues((JSONWorkers) =>
JSONWorkers.map((JSONWorker) => {
if (JSONWorker.changeUniqueWorker) {
return [JSONWorker.changeUniqueWorker.replace(/\s/g, ''), JSONWorker.uniqueWorkerId.replace(/\s/g, '')];
}
return [JSONWorker.uniqueWorkerId.replace(/\s/g, '')];
}),
)
.mapValues((workerRefs) => new Set(workerRefs.flat()))
.value();
};

module.exports = {
crossValidate,
_crossValidateMainJobRole,
crossValidateTransferStaffRecord,
_isCQCRegulated,
};
57 changes: 57 additions & 0 deletions backend/server/models/BulkImport/csv/crossValidateErrors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const MAIN_JOB_ROLE_ERROR_CODE = 1280;
const TRANSFER_STAFF_RECORD_BASE_ERROR_CODE = 1400;

const MAIN_JOB_ERRORS = {
RegisteredManagerWithoutCqcRegulatedService: Object.freeze({
errCode: MAIN_JOB_ROLE_ERROR_CODE,
errType: 'MAIN_JOB_ROLE_ERROR',
column: 'MAINJOBROLE',
_sourceFieldName: 'mainJobRoleId',
error:
'Workers MAINJOBROLE is Registered Manager but you are not providing a CQC regulated service. Please change to another Job Role',
}),
};

const TRANSFER_STAFF_RECORD_ERRORS = {
NewWorkplaceNotFound: Object.freeze({
errCode: TRANSFER_STAFF_RECORD_BASE_ERROR_CODE + 1,
errType: 'TRANSFERSTAFFRECORD_ERROR',
column: 'TRANSFERSTAFFRECORD',
_sourceFieldName: 'transferStaffRecord',
error: 'The LOCALESTID in TRANSFERSTAFFRECORD does not exist',
}),
SameLocalIdExistInNewWorkplace: Object.freeze({
errCode: TRANSFER_STAFF_RECORD_BASE_ERROR_CODE + 2,
errType: 'TRANSFERSTAFFRECORD_ERROR',
column: 'UNIQUEWORKERID',
_sourceFieldName: 'uniqueWorkerId',
error:
"The UNIQUEWORKERID already exists in the LOCALESTID given in TRANSFERSTAFFRECORD. Use CHGUNIQUEWRKID to change this worker's UNIQUEWORKERID",
}),
SameRefsMovingToWorkplace: Object.freeze({
errCode: TRANSFER_STAFF_RECORD_BASE_ERROR_CODE + 3,
errType: 'TRANSFERSTAFFRECORD_ERROR',
column: 'UNIQUEWORKERID',
_sourceFieldName: 'uniqueWorkerId',
error: 'Duplicate UNIQUEWORKERID’s are being moved to the same LOCALESTID in TRANSFERSTAFFRECORD',
}),
};

const addCrossValidateError = (errorsArray, errorType, JSONWorker) => {
const newErrorObject = {
...errorType,
worker: JSONWorker.uniqueWorkerId,
name: JSONWorker.localId,
lineNumber: JSONWorker.lineNumber,
source: JSONWorker[errorType._sourceFieldName],
};
delete newErrorObject._sourceFieldName;

errorsArray.unshift(newErrorObject);
};

module.exports = {
addCrossValidateError,
MAIN_JOB_ERRORS,
TRANSFER_STAFF_RECORD_ERRORS,
};
Original file line number Diff line number Diff line change
Expand Up @@ -2780,7 +2780,7 @@ class WorkplaceCSVValidator {

let registeredManagers = 0;

const dataInCSV = ['NEW', 'UPDATE', 'CHGSUB']; //For theses statuses trust the data in the CSV
const dataInCSV = ['NEW', 'UPDATE']; //For theses statuses trust the data in the CSV

myJSONWorkers.forEach((worker) => {
if (this.key === worker.establishmentKey && dataInCSV.includes(worker.status)) {
Expand Down
36 changes: 33 additions & 3 deletions backend/server/models/classes/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const { v4: uuidv4 } = require('uuid');
uuidv4();

// database models
const { Op } = require('sequelize');
const models = require('../index');

const EntityValidator = require('./validations/entityValidator').EntityValidator;
Expand Down Expand Up @@ -75,6 +76,8 @@ class Worker extends EntityValidator {

// bulk upload status - this is never stored in database
this._status = bulkUploadStatus;

this._transferStaffRecord = null;
}

// returns true if valid establishment id
Expand Down Expand Up @@ -193,6 +196,14 @@ class Worker extends EntityValidator {
return this._status;
}

get transferStaffRecord() {
return this._transferStaffRecord;
}

get newWorkplaceId() {
return this._newWorkplaceId;
}

get contract() {
return this._properties.get('Contract') ? this._properties.get('Contract').property : null;
}
Expand Down Expand Up @@ -355,6 +366,14 @@ class Worker extends EntityValidator {
this._status = document.status;
}

if (document.transferStaffRecord) {
this._transferStaffRecord = document.transferStaffRecord;
}

if (document.newWorkplaceId) {
this._newWorkplaceId = document.newWorkplaceId;
}

// Consequential updates when one value means another should be empty or null

// If their job isn't a registered nurse, remove their specialism and category
Expand Down Expand Up @@ -549,11 +568,11 @@ class Worker extends EntityValidator {
currentTrainingRecord.workerId = this._id;
currentTrainingRecord.workerUid = this._uid;
currentTrainingRecord.establishmentId = this._establishmentId;
newTrainingPromises.push(currentTrainingRecord.save(savedBy, bulkUploaded, 0, externalTransaction));
newTrainingPromises.push(currentTrainingRecord.save(savedBy, bulkUploaded, externalTransaction));
});
}

if (bulkUploaded && ['NEW', 'UPDATE', 'CHGSUB'].includes(this.status)) {
if (bulkUploaded && ['NEW', 'UPDATE'].includes(this.status)) {
const qualificationHelper = new BulkUploadQualificationHelper({
workerId: this._id,
workerUid: this._uid,
Expand Down Expand Up @@ -670,7 +689,6 @@ class Worker extends EntityValidator {
if (associatedEntities) {
await this.saveAssociatedEntities(savedBy, bulkUploaded, thisTransaction);
}

if (this.nurseSpecialisms && this.nurseSpecialisms.value === 'Yes') {
await models.workerNurseSpecialisms.bulkCreate(
this.nurseSpecialisms.specialisms.map((thisSpecialism) => ({
Expand Down Expand Up @@ -747,6 +765,10 @@ class Worker extends EntityValidator {
updatedBy: savedBy.toLowerCase(),
};

if (bulkUploaded && this._status === 'UPDATE' && this.transferStaffRecord && this.newWorkplaceId) {
updateDocument.establishmentFk = this.newWorkplaceId;
}

if (this._changeLocalIdentifer) {
// during bulk upload only, if the change local identifier value is set, then when saving this worker, update it's local identifier;
updateDocument.LocalIdentifierValue = this._changeLocalIdentifer;
Expand Down Expand Up @@ -1352,6 +1374,14 @@ class Worker extends EntityValidator {
myDefaultJSON.status = this._status;
}

if (this._transferStaffRecord !== null) {
myDefaultJSON.transferStaffRecord = this._transferStaffRecord;
}

if (this._newWorkplaceId !== null) {
myDefaultJSON.newWorkplaceId = this._newWorkplaceId;
}

// TODO: JSON schema validation
if (showHistory && !showPropertyHistoryOnly) {
return {
Expand Down
Loading

0 comments on commit 5191be9

Please sign in to comment.