diff --git a/.env.sample b/.env.sample index f6307ace..3692dcec 100644 --- a/.env.sample +++ b/.env.sample @@ -30,4 +30,13 @@ SUBMISSION_TOPIC = "dev.sl.projects.submissions" PROJECT_SUBMISSION_TOPIC = "dev.sl.projects.submissions" // project submission topic # SUNBIRD LOCATION AND USER READ -USER_SERVICE_URL = "http://user-service:3000" // service used for user profile read location search are using this base url \ No newline at end of file +USER_SERVICE_URL = "http://user-service:3000" // service used for user profile read location search are using this base url + +#service name +SERVICE_NAME = ml-project-service // ml-project service name + +# sunbird-rc service +CERTIFICATE_SERVICE_URL = http://registry-service:8081 // sunbird-RC registry service URL + +PROJECT_CERTIFICATE_ON_OFF = "ON/OFF" // Project certificate enable or disable flag + diff --git a/config/globals.js b/config/globals.js index ad63ab7a..c14c6428 100644 --- a/config/globals.js +++ b/config/globals.js @@ -76,6 +76,7 @@ module.exports = function () { global.schemas[name] = require(PROJECT_ROOT_DIRECTORY + '/models/' + file); } }); + // All controllers global.controllers = requireAll({ diff --git a/config/kafka.js b/config/kafka.js index 3d248170..3e959a19 100644 --- a/config/kafka.js +++ b/config/kafka.js @@ -9,6 +9,7 @@ //dependencies const kafka = require('kafka-node'); const SUBMISSION_TOPIC = process.env.SUBMISSION_TOPIC; +const CERTIFICATE_TOPIC = process.env.PROJECT_SUBMISSION_TOPIC; /** * Kafka configurations. @@ -44,6 +45,12 @@ const connect = function() { process.env.KAFKA_URL ); + // project certificate details consumer + _sendToKafkaConsumers( + CERTIFICATE_TOPIC, + process.env.KAFKA_URL + ); + return { kafkaProducer: producer, kafkaClient: client @@ -82,6 +89,10 @@ var _sendToKafkaConsumers = function (topic,host) { if (message && message.topic === SUBMISSION_TOPIC) { submissionsConsumer.messageReceived(message); } + // call projectCertificateConsumer + if (message && message.topic === CERTIFICATE_TOPIC) { + projectCertificateConsumer.messageReceived(message); + } }); @@ -90,6 +101,9 @@ var _sendToKafkaConsumers = function (topic,host) { if(error.topics && error.topics[0] === SUBMISSION_TOPIC) { submissionsConsumer.errorTriggered(error); } + if(error.topics && error.topics[0] === CERTIFICATE_TOPIC) { + projectCertificateConsumer.errorTriggered(error); + } }); diff --git a/controllers/v1/certificateTemplates.js b/controllers/v1/certificateTemplates.js new file mode 100644 index 00000000..c57fa739 --- /dev/null +++ b/controllers/v1/certificateTemplates.js @@ -0,0 +1,16 @@ +/** + * name : certificateTemplates.js + * author : Vishnu + * created-date : 29-Sep-2022 + * Description : Certificate template related information. +*/ + +module.exports = class CertificateTemplates extends Abstract { + constructor() { + super("certificateTemplates"); + } + + static get name() { + return "certificateTemplates"; + } +} \ No newline at end of file diff --git a/controllers/v1/userProjects.js b/controllers/v1/userProjects.js index e8019d26..052a4f9b 100644 --- a/controllers/v1/userProjects.js +++ b/controllers/v1/userProjects.js @@ -126,7 +126,6 @@ module.exports = class UserProjects extends Abstract { async sync(req) { return new Promise(async (resolve, reject) => { try { - let createdProject = await userProjectsHelper.sync( req.params._id, req.query.lastDownloadedAt, @@ -957,4 +956,174 @@ module.exports = class UserProjects extends Abstract { }) } + /** + * @api {post} /improvement-project/api/v1/userProjects/certificateCallback + * Project certificate callback + * @apiVersion 1.0.0 + * @apiGroup User Projects + * @apiSampleRequest /improvement-project/api/v1/userProjects/certificateCallback + * @apiParamExample {json} Request + * { + "event": "sunbird-rc-create", + "timestamp": 1660145509358, + "data": { + "userId": "anonymous", + "entityType": "ProjectCertificate", + "osid": "ce2244a4-1a17-49a0-a3f9-c151161e70bl", + "transactionId": "1-3a4892d8-2221-4e96-9434-f4b37886126b", + "status": "SUCCESSFUL", + "message": "" + }, + "webhookUrl": "http://ml-project-service:3000/v1/userProjects/certificateCallback" + } + * @apiParamExample {json} Response: + /**{ + "message": "Successfully generated project certificate", + "status": 200, + "result": { + "_id": "63446059eeffea2b819f036e" + } + } + /** + + /** + * Project certificate callback. + * @method + * @name certificateCallback + * @param {Object} req - request data. + * @returns {JSON} certificate details. + */ + + async certificateCallback(req) { + return new Promise(async (resolve, reject) => { + try { + //console request body to check if callback is coming or not and to check any structural change is there or not + console.log("-------------callback request body------------",JSON.stringify(req.body)) + let certificateDetails = await userProjectsHelper.certificateCallback( req.body.data.transactionId, req.body.data.osid ); + return resolve({ + message: certificateDetails.message, + result: certificateDetails.data + }); + } catch (error) { + return reject({ + status: error.status || HTTP_STATUS_CODE.internal_server_error.status, + message: error.message || HTTP_STATUS_CODE.internal_server_error.message, + errorObject: error + }); + } + }) + } + + /** + * @api {get} /improvement-project/api/v1/userProjects/certificates + * List of user project with certificate + * @apiVersion 1.0.0 + * @apiGroup User Projects + * @apiSampleRequest /improvement-project/api/v1/userProjects/certificates + * @apiParamExample {json} Response: + * { + "message": "User project fetched successfully", + "status": 200, + "result": { + "data": [{ + "_id": "60793b80bd49095a19ddeae1", + "title": "Project with learning resources", + "certificate": { + "osid": "1-21c8ecab-7b8d-40f1-9961-cae7fcb6a5f9", + "status": "active", + "templateId": "600acc42c7de076e6f995147", + "templateUrl": "certificateTemplates/6343bd978f9d8980b7841e85/ba9aa220-ff1b-4717-b6ea-ace55f04fc16_2022-9-10-1665383945769.svg", + "issuedOn": "2020-12-03 13:22:31.988Z" + }, + "status": "submitted" + }, + { + "_id": "6011136a2d25b926974d9ec9", + "title": "Keep Our Schools Alive! (Petition)", + "status": "submitted", + "certificate": { + "eligible": false, + "templateId": "600acc42c7de076e6f995147", + "message": "Not submitted the project the project within program end date" + } + } + ], + "count": 2, + "certificateCount": 1 + } + } + /** + + /** + * List user project details with certificate + * @method + * @name certificates + * @returns {JSON} User project detaills with certificate + */ + + async certificates(req) { + return new Promise(async (resolve, reject) => { + try { + // fetch projects data of user, whish has certificate on completion + let projectDetails = await userProjectsHelper.certificates( req.userDetails.userInformation.userId ); + return resolve({ + message: projectDetails.message, + result: projectDetails.data + }); + + } catch (error) { + return reject({ + status: error.status || HTTP_STATUS_CODE.internal_server_error.status, + message: error.message || HTTP_STATUS_CODE.internal_server_error.message, + errorObject: error + }); + } + }) + } + + /** + * @api {post} /improvement-project/api/v1/userProjects/certificateReIssue + * ReIssue project certificate (admin api) + * @apiVersion 1.0.0 + * @apiGroup User Projects + * @apiSampleRequest /improvement-project/api/v1/userProjects/certificateReIssue + * @apiParamExample {json} Response: + /**{ + "message": "Successfully generated project certificate", + "status": 200, + "result": { + "_id": "63446059eeffea2b819f036e" + } + } + /** + * ReIssue project certificate + * @method + * @name certificateReIssue + * @returns {JSON} Reissued details + */ + + async certificateReIssue(req) { + return new Promise(async (resolve, reject) => { + try { + // ReIssue certificate of given project : projectId is passed as param + // This console has to be removed- adding to check the Issuer kid value in case rancher doesn't display console while deployment + console.log("+++++CERTIFICATE_ISSUER_KID+++++ : ",CERTIFICATE_ISSUER_KID) + let projectDetails = await userProjectsHelper.certificateReIssue( + req.params._id, + ); + return resolve({ + message: projectDetails.message, + result: projectDetails.data + }); + + } catch (error) { + return reject({ + status: error.status || HTTP_STATUS_CODE.internal_server_error.status, + message: error.message || HTTP_STATUS_CODE.internal_server_error.message, + errorObject: error + }); + } + }) + } + }; \ No newline at end of file diff --git a/databaseQueries/certificateTemplates.js b/databaseQueries/certificateTemplates.js new file mode 100644 index 00000000..fa2a2ddc --- /dev/null +++ b/databaseQueries/certificateTemplates.js @@ -0,0 +1,60 @@ +/** + * name : certificateTemplates.js + * author : Vishnu + * created-date : 03-Oct-2022 + * Description : Certificate template helper for DB interactions. + */ + +// Dependencies + +/** + * CertificateTemplates + * @class +*/ + +module.exports= class CertificateTemplates{ + /** + * certificate template details. + * @method + * @name certificateTemplateDocument + * @param {Array} [filterData = "all"] - certificate template filter query. + * @param {Array} [fieldsArray = "all"] - projected fields. + * @param {Array} [skipFields = "none"] - field not to include + * @returns {Array} certificateTemplates details. + */ + + static certificateTemplateDocument( + filterData = "all", + fieldsArray = "all", + skipFields = "none" + ) { + return new Promise(async (resolve, reject) => { + try { + let queryObject = (filterData != "all") ? filterData : {}; + let projection = {} + + if (fieldsArray != "all") { + fieldsArray.forEach(field => { + projection[field] = 1; + }); + } + + if( skipFields !== "none" ) { + skipFields.forEach(field=>{ + projection[field] = 0; + }); + } + let certificateTemplateDoc = + await database.models.certificateTemplates.find( + queryObject, + projection + ).lean(); + + return resolve(certificateTemplateDoc); + + } catch (error) { + return reject(error); + } + }); + } +} \ No newline at end of file diff --git a/envVariables.js b/envVariables.js index 8ce15ac4..5228d7ef 100644 --- a/envVariables.js +++ b/envVariables.js @@ -8,6 +8,7 @@ const Log = require("log"); let log = new Log("debug"); let table = require("cli-table"); +const certificateService = require(GENERICS_FILES_PATH + "/services/certificate"); let tableData = new table(); @@ -39,6 +40,26 @@ let enviromentVariables = { "USER_SERVICE_URL" : { "message" : "Required user service base url", "optional" : false + }, + "SERVICE_NAME" : { + "message" : "current service name", + "optional" : true, + "default" : "ml-projects-service" + }, + "CERTIFICATE_SERVICE_URL" : { + "message" : "certificate service base url", + "optional" : true, + "default" : "http://registry-service:8081", + "requiredIf" : { + "key": "PROJECT_CERTIFICATE_ON_OFF", + "operator" : "EQUALS", + "value" : "ON" + } + }, + "PROJECT_CERTIFICATE_ON_OFF" : { + "message" : "Enable/Disable project certification", + "optional" : false, + "default" : "ON" } } @@ -52,7 +73,7 @@ module.exports = function() { }; let keyCheckPass = true; - + let validRequiredIfOperators = ["EQUALS","NOT_EQUALS"] if(enviromentVariables[eachEnvironmentVariable].optional === true && enviromentVariables[eachEnvironmentVariable].requiredIf @@ -99,6 +120,8 @@ module.exports = function() { && enviromentVariables[eachEnvironmentVariable].default && enviromentVariables[eachEnvironmentVariable].default != "") { process.env[eachEnvironmentVariable] = enviromentVariables[eachEnvironmentVariable].default; + success = true; + keyCheckPass = true; } if(!keyCheckPass) { @@ -109,16 +132,35 @@ module.exports = function() { tableObj[eachEnvironmentVariable] = `FAILED - ${eachEnvironmentVariable} is required`; } } - tableData.push(tableObj); }) log.info(tableData.toString()); - + getKid(); return { success : success } } +async function getKid(){ + if ( process.env.PROJECT_CERTIFICATE_ON_OFF === "ON" ) { + // get certificate issuer kid from sunbird-RC + let kidData = await certificateService.getCertificateIssuerKid(); + if( !kidData.success ) { + console.log("failed to get kid value from registry service : ",kidData) + if( process.env.CERTIFICATE_ISSUER_KID && process.env.CERTIFICATE_ISSUER_KID != "" ) { + global.CERTIFICATE_ISSUER_KID = process.env.CERTIFICATE_ISSUER_KID; + } + // console.log("Server stoped . Failed to set certificate issuer Kid value") + // process.exit(); + } else { + console.log("Kid data fetched successfully : ",kidData.data) + global.CERTIFICATE_ISSUER_KID = kidData.data + } + console.log(JSON.stringify(kidData)) + // global.CERTIFICATE_ISSUER_KID = kidData.data + } +}; + diff --git a/generics/constants/api-responses.js b/generics/constants/api-responses.js index 9b85a71f..568265f4 100644 --- a/generics/constants/api-responses.js +++ b/generics/constants/api-responses.js @@ -124,5 +124,16 @@ module.exports = { "TEMPLATE_ID_OR_LINK_REQUIRED" : "TemplateId or Link either one is required", "TEMPLATE_ID_NOT_FOUND_IN_SOLUTION" : "Could not found templateId in solution", "FAILED_TO_SYNC_PROJECT_ALREADY_SUBMITTED" : "Failed to sync, Project is already Submitted", - "SOLUTION_ID_AND_USERPROFILE_REQUIRED": "Required solution Id and userProfile" + "SOLUTION_ID_AND_USERPROFILE_REQUIRED": "Required solution Id and userProfile", + "PROJECT_WITH_CERTIFICATE_NOT_FOUND": "No certification project found for user", + "PROJECT_CERTIFICATE_GENERATED" : "Successfully generated project certificate", + "TRANSACTION_ID_AND_OSID_REQUIRED" : "Required transactionId and osid", + "PROJECT_CERTIFICATE_GENERATED_ONCE" : "Certificate generated once", + "DOWNLOADABLE_URL_NOT_FOUND" : "Failed to generate downloadable URL", + "CERTIFICATE_TEMPLATE_NOT_FOUND" : "Certificate template details not found", + "CERTIFICATE_GENERATION_FAILED" : "Certificate generation failed", + "NOT_ELIGIBLE_FOR_CERTIFICATE" : "Project is not eligible for certificate", + "ISSUER_KID_NOT_FOUND" : "Failed to fetch certificate issuer kid", + "PROJECT_SUBMITTED_FOR_REISSUE" : "Submitted for project certificate reIssue", + "FAILED_TO_START_RESOURCE": "There was an error in starting/joining. Please try again after some time." }; diff --git a/generics/constants/common.js b/generics/constants/common.js index dd75260a..30e2703c 100644 --- a/generics/constants/common.js +++ b/generics/constants/common.js @@ -50,5 +50,6 @@ module.exports = { "DISTRICT": "district", "SERVER_TIME_OUT" : 5000, "OK" : "OK", - "PROJECT" : "project" + "PROJECT" : "project", + "PROJECT_CERTIFICATE_GENERATED_SUCCESSFULLY" : "Certificate generated successfully" }; diff --git a/generics/constants/endpoints.js b/generics/constants/endpoints.js index 11c43bb4..c8a5bfaf 100644 --- a/generics/constants/endpoints.js +++ b/generics/constants/endpoints.js @@ -47,5 +47,9 @@ module.exports = { FILES_DOWNLOADABLE_URL: "/v1/cloud-services/files/getDownloadableUrl", OBSERVATION_DETAILS : "/v1/observations/details", USER_READ_V5 : "/v5/user/read", - GET_LOCATION_DATA : "/v1/location/search" + GET_LOCATION_DATA : "/v1/location/search", + CERTIFICATE_CREATE : "/api/v1/ProjectCertificate", + PROJECT_CERTIFICATE_API_CALLBACK : "/v1/userProjects/certificateCallback", + USER_READ_PRIVATE : "/private/user/v1/read", // !Caution: End point for reading user details without token. Do not use for public work flow + GET_CERTIFICATE_KID : "/api/v1/PublicKey/search" }; diff --git a/generics/helpers/utils.js b/generics/helpers/utils.js index 58d92e77..900d00e3 100644 --- a/generics/helpers/utils.js +++ b/generics/helpers/utils.js @@ -264,6 +264,102 @@ function checkValidUUID(uuids) { } return validateUUID; } + +/** + * make dates comparable + * @function + * @name createComparableDates + * @param {String} dateArg1 + * @param {String} dateArg2 + * @returns {Object} - date object +*/ + +function createComparableDates(dateArg1, dateArg2) { + let date1 + if(typeof dateArg1 === "string") { + date1 = new Date(dateArg1.replace( /(\d{2})-(\d{2})-(\d{4})/, "$2/$1/$3")) + } else { + date1 = new Date(dateArg1) + } + + let date2 + if(typeof dateArg2 === "string") { + date2 = new Date(dateArg2.replace( /(\d{2})-(\d{2})-(\d{4})/, "$2/$1/$3")) + } else { + date2 = new Date(dateArg2) + } + + date1.setHours(0) + date1.setMinutes(0) + date1.setSeconds(0) + date2.setHours(0) + date2.setMinutes(0) + date2.setSeconds(0) + return({ + dateOne: date1, + dateTwo: date2 + }) +} + +/** + * count attachments + * @function + * @name noOfElementsInArray + * @param {Object} data - data to count + * @param {Object} filter - filter data + * @returns {Number} - attachment count +*/ + +function noOfElementsInArray(data, filter = {}) { + if ( !filter || !Object.keys(filter).length > 0 ) { + return data.length; + } + if ( !data.length > 0 ) { + return 0; + } else { + if ( filter.value == "all" ){ + return data.length; + } else { + let count = 0; + for ( let attachment = 0; attachment < data.length; attachment++ ) { + if ( data[attachment][filter.key] == filter.value ) { + count++ + } + } + return count; + } + } +} + +/** + * validate lhs and rhs using operator passed as String/ Number + * @function + * @name operatorValidation + * @param {Number or String} valueLhs + * @param {Number or String} valueRhs + * @returns {Boolean} - validation result +*/ + +function operatorValidation(valueLhs, valueRhs, operator) { + return new Promise(async (resolve, reject) => { + let result = false; + if (operator == "==" ) { + result = (valueLhs == valueRhs) ? true : false + } else if (operator == "!=" ) { + result = (valueLhs != valueRhs) ? true : false + } else if (operator == ">" ) { + result = (valueLhs > valueRhs) ? true : false + } else if (operator == "<" ) { + result = (valueLhs < valueRhs) ? true : false + } else if (operator == "<=" ) { + result = (valueLhs <= valueRhs) ? true : false + } else if (operator == ">=" ) { + result = (valueLhs >= valueRhs) ? true : false + } + return resolve(result) + }) +} + module.exports = { camelCaseToTitleCase : camelCaseToTitleCase, lowerCase : lowerCase, @@ -277,5 +373,8 @@ module.exports = { convertProjectStatus : convertProjectStatus, revertProjectStatus:revertProjectStatus, revertStatusorNot:revertStatusorNot, - checkValidUUID : checkValidUUID + checkValidUUID : checkValidUUID, + createComparableDates : createComparableDates, + noOfElementsInArray : noOfElementsInArray, + operatorValidation : operatorValidation }; diff --git a/generics/kafka/consumers/projectCertificate.js b/generics/kafka/consumers/projectCertificate.js new file mode 100644 index 00000000..66823a75 --- /dev/null +++ b/generics/kafka/consumers/projectCertificate.js @@ -0,0 +1,62 @@ +/** + * name : projectCertificate.js + * author : Vishnu + * created-date : 10-Oct-2022 + * Description : Project certificates submission consumer. +*/ + +//dependencies +const userProjectsHelper = require(MODULES_BASE_PATH + "/userProjects/helper"); + +/** +* submission consumer message received. +* @function +* @name messageReceived +* @param {String} message - consumer data +* @returns {Promise} return a Promise. +*/ + +var messageReceived = function (message) { + return new Promise(async function (resolve, reject) { + try { + // This consumer is consuming from an old topic : PROJECT_CERTIFICATE_TOPIC, which is no more used by data team. ie) using existig topic instead of creating new one. + let parsedMessage = JSON.parse( message.value ); + if ( parsedMessage.status == CONSTANTS.common.SUBMITTED_STATUS && + parsedMessage.certificate && + Object.keys(parsedMessage.certificate).length > 0 && + !parsedMessage.certificate.eligible + ) { + await userProjectsHelper.generateCertificate( parsedMessage ); + } + return resolve("Message Received"); + } catch (error) { + return reject(error); + } + + }); +}; + +/** +* If message is not received. +* @function +* @name errorTriggered +* @param {Object} error - error object +* @returns {Promise} return a Promise. +*/ + +var errorTriggered = function (error) { + return new Promise(function (resolve, reject) { + + try { + return resolve(error); + } catch (error) { + return reject(error); + } + + }); +}; + +module.exports = { + messageReceived: messageReceived, + errorTriggered: errorTriggered +}; diff --git a/generics/kafka/consumers/submissions.js b/generics/kafka/consumers/submissions.js index 9c56c4d3..e2411f53 100644 --- a/generics/kafka/consumers/submissions.js +++ b/generics/kafka/consumers/submissions.js @@ -25,7 +25,7 @@ var messageReceived = function (message) { try { let parsedMessage = JSON.parse(message.value); - + let submissionDocument = { "_id" : parsedMessage._id.toString(), "status" : parsedMessage.status, diff --git a/generics/middleware/authenticator.js b/generics/middleware/authenticator.js index b3599204..b34628ab 100644 --- a/generics/middleware/authenticator.js +++ b/generics/middleware/authenticator.js @@ -49,7 +49,7 @@ module.exports = async function (req, res, next, token = "") { // Allow search endpoints for non-logged in users. let guestAccess = false; - let guestAccessPaths = ["/dataPipeline/","/templates/details"]; + let guestAccessPaths = ["/dataPipeline/","/templates/details","userProjects/certificateCallback"]; await Promise.all(guestAccessPaths.map(async function (path) { if (req.path.includes(path)) { guestAccess = true; diff --git a/generics/services/certificate.js b/generics/services/certificate.js new file mode 100644 index 00000000..1e0d4583 --- /dev/null +++ b/generics/services/certificate.js @@ -0,0 +1,127 @@ +/** + * name : certificate.js + * author : Vishnu + * Date : 07-Oct-2022 + * Description : Sunbird-RC certificate api. + */ + +//dependencies +const request = require('request'); + +/** + * Project certificate creation + * @function + * @name createCertificate + * @param {Object} bodyData - Body data + * @returns {JSON} - Certificate creation details. +*/ + +const createCertificate = function (bodyData) { + return new Promise(async (resolve, reject) => { + try { + + const ML_PROJECT_URL = `http://${process.env.SERVICE_NAME}:${process.env.APPLICATION_PORT}`; + const callbackUrl = ML_PROJECT_URL + CONSTANTS.endpoints.PROJECT_CERTIFICATE_API_CALLBACK; + let certificateCreateUrl = + process.env.CERTIFICATE_SERVICE_URL + + CONSTANTS.endpoints.CERTIFICATE_CREATE + "?mode=async&callback=" + callbackUrl; + const options = { + headers : { + "content-type": "application/json" + }, + json : bodyData + }; + + console.log("bodyData : ",bodyData) + console.log("certificateCreateUrl : ",certificateCreateUrl) + + request.post(certificateCreateUrl,options,certificateCallback); + + function certificateCallback(err, data) { + console.log("line 41 raw data from RC call :",JSON.stringify(data)); + let result = { + success : true + }; + if (err) { + result.success = false; + console.log("line 45 error from RC call error :",err.message); + } else { + let response = data.body; + console.log("certificate success response: ",JSON.stringify(response)) + if( response.params && response.params.status && response.params.status === "SUCCESSFUL" ) { + result["data"] = response.result; + } else { + result.success = false; + } + } + return resolve(result); + } + + } catch (error) { + console.log("line 58 catch block : ",error.message) + return reject(error); + } + }) +} + +/** + * Project certificate issuer-kid + * @function + * @name getCertificateIssuerKid + * @returns {JSON} - Certificate issuer kid details. +*/ + +const getCertificateIssuerKid = function () { + return new Promise(async (resolve, reject) => { + try { + let issuerKidUrl = + process.env.CERTIFICATE_SERVICE_URL + CONSTANTS.endpoints.GET_CERTIFICATE_KID; + + let bodyData = {"filters": {}}; + + const options = { + headers : { + "Content-Type": "application/json" + }, + json : bodyData + }; + console.log("issuer Kid url : ",issuerKidUrl); + console.log("issuer Kid bodyData : ",JSON.stringify(bodyData)); + request.post(issuerKidUrl,options,getKidCallback); + function getKidCallback(err, data) { + console.log("line 92 raw data from KID call :",JSON.stringify(data)); + let result = { + success : true + }; + + if (err) { + console.log("KID rc call error : ",err.message) + result.success = false; + } else { + let response = data.body; + console.log("KID success response : ",JSON.stringify(response)) + if( response.length > 0 && response[0].osid && response[0].osid !== "" ) { + result["data"] = response[0].osid; + } else { + result.success = false; + } + } + return resolve(result); + } + setTimeout(function () { + return resolve (result = { + success : false + }); + }, CONSTANTS.common.SERVER_TIME_OUT); + + } catch (error) { + console.log("catch error : ",error.message) + return reject(error); + } + }) +} + +module.exports = { + createCertificate : createCertificate, + getCertificateIssuerKid : getCertificateIssuerKid +} \ No newline at end of file diff --git a/generics/services/report.js b/generics/services/report.js index 765e9ea7..75e58b9a 100644 --- a/generics/services/report.js +++ b/generics/services/report.js @@ -118,8 +118,6 @@ const projectAndTaskReport = function (token, input, projectPdf) { const url = reportsUrl + CONSTANTS.endpoints.PROJECT_AND_TASK_REPORT + "?projectPdf=" + projectPdf; - - console.log("--- url is- ----",url); let options = { headers : { @@ -147,7 +145,6 @@ const projectAndTaskReport = function (token, input, projectPdf) { } } catch (error) { - console.log("catch error",error); return reject(error); } }) diff --git a/generics/services/users.js b/generics/services/users.js index 53f8edae..57c00fdf 100644 --- a/generics/services/users.js +++ b/generics/services/users.js @@ -176,8 +176,57 @@ async function getParentEntities( entityId, iteration = 0, parentEntities ) { return parentEntities; } + +/** + * get user profileData without token. + * @method + * @name profileReadPrivate + * @param {String} userId - user Id + * @returns {JSON} - User profile details +*/ +const profileReadPrivate = function (userId) { + return new Promise(async (resolve, reject) => { + try { + // <--- Important : This url endpoint is private do not use it for regular workflows ---> + let url = userServiceUrl + CONSTANTS.endpoints.USER_READ_PRIVATE + "/" + userId; + const options = { + headers : { + "content-type": "application/json" + } + }; + request.get(url,options,userReadCallback); + let result = { + success : true + }; + function userReadCallback(err, data) { + if (err) { + result.success = false; + } else { + + let response = JSON.parse(data.body); + if( response.responseCode === HTTP_STATUS_CODE['ok'].code ) { + result["data"] = response.result; + } else { + result.success = false; + } + + } + return resolve(result); + } + setTimeout(function () { + return resolve (result = { + success : false + }); + }, CONSTANTS.common.SERVER_TIME_OUT); + + } catch (error) { + return reject(error); + } + }) +} module.exports = { profile : profile, locationSearch : locationSearch, - getParentEntities : getParentEntities + getParentEntities : getParentEntities, + profileReadPrivate : profileReadPrivate }; diff --git a/migrations/updateProjectDocuments/migratePrivateProjectToPublicProgram.js b/migrations/updateProjectDocuments/migratePrivateProjectToPublicProgram.js new file mode 100644 index 00000000..1a5866df --- /dev/null +++ b/migrations/updateProjectDocuments/migratePrivateProjectToPublicProgram.js @@ -0,0 +1,166 @@ +/** + * name : updatePrivateProgramInProject.js + * author : Ankit Shahu + * created-date : 02-Feb-2023 + * Description : Migration script for update project + */ + + const path = require("path"); + let rootPath = path.join(__dirname, '../../') + require('dotenv').config({ path: rootPath+'/.env' }) + + let _ = require("lodash"); + let mongoUrl = process.env.MONGODB_URL; + let dbName = mongoUrl.split("/").pop(); + let url = mongoUrl.split(dbName)[0]; + var MongoClient = require('mongodb').MongoClient; + var ObjectId = require('mongodb').ObjectID; + + var fs = require('fs'); + + +(async () => { + + let connection = await MongoClient.connect(url, { useNewUrlParser: true }); + let db = connection.db(dbName); + try { + + let updatedProjectIds = []; + let deletedSolutionIds = []; + let deletedProgramIds = []; + + + + //get all projectss id where user profile is not there. + let projectDocument = await db.collection('projects').find({ + userRoleInformation: {$exists : false}, + isAPrivateProgram: true, + }).project({_id:1,userProfile:1}).toArray(); + + + let chunkOfProjectDocument = _.chunk(projectDocument, 10); + // console.log(chunkOfProjectDocument) + let projectIds; + + for (let pointerToProject = 0; pointerToProject < chunkOfProjectDocument.length; pointerToProject++) { + projectIds = await chunkOfProjectDocument[pointerToProject].map( + projectDoc => { + return projectDoc._id; + } + ); + + + // get project documents from projectss collection in Array + let projectDocuments = await db.collection('projects').find({ + _id: { $in :projectIds } + }).project({_id:1,userProfile:1}).toArray(); + //iterate project documents one by one + for(let counter = 0; counter < projectDocuments.length; counter++) { + + + if(projectDocuments[counter].hasOwnProperty("solutionId") && projectDocuments[counter].isAPrivateProgram){ + // find solution document form solution collection + let solutionDocument = await db.collection('solutions').find({ + _id: projectDocuments[counter].solutionId, + parentSolutionId : {$exists:true}, + isAPrivateProgram : true + }).project({}).toArray({}) + //find program document form program collection + if(solutionDocument.length == 1){ + + // find parent solution document in same collection + let parentSolutionDocument = await db.collection('solutions').find({ + _id: solutionDocument[0].parentSolutionId}).project({}).toArray({}); + //varibale to update project document + let updateProjectDocument = { + "$set" : {} + }; + updateProjectDocument["$set"]["solutionId"] = parentSolutionDocument[0]._id + updateProjectDocument["$set"]["isAPrivateProgram"] = parentSolutionDocument[0].isAPrivateProgram + updateProjectDocument["$set"]["solutionInformation"] = { + name: parentSolutionDocument[0].name, + description: parentSolutionDocument[0].description, + externalId: parentSolutionDocument[0].externalId, + _id: parentSolutionDocument[0]._id, + } + updateProjectDocument["$set"]["solutionExternalId"] = parentSolutionDocument[0].externalId, + updateProjectDocument["$set"]["programId"] = parentSolutionDocument[0].programId, + updateProjectDocument["$set"]["programExternalId"] = parentSolutionDocument[0].programExternalId + updateProjectDocument["$set"]["programInformation"] = { + _id : parentSolutionDocument[0].programId, + name : parentSolutionDocument[0].programName, + externalId : parentSolutionDocument[0].programExternalId, + description : parentSolutionDocument[0].programDescription, + isAPrivateProgram : parentSolutionDocument[0].isAPrivateProgram + } + if(projectDocuments[counter].hasOwnProperty("userProfile")) + { + let userLocations = projectDocuments[counter].userProfile.userLocations + let userRoleInfomration = {} + //get data in userRoleInfomration key + for(let userLocationCounter = 0; userLocationCounter < userLocations.length; userLocationCounter++){ + if(userLocations[userLocationCounter].type !== "school"){ + userRoleInfomration[userLocations[userLocationCounter].type] = userLocations[userLocationCounter].id + }else{ + userRoleInfomration[userLocations[userLocationCounter].type] = userLocations[userLocationCounter].code + } + } + let Roles = "" + for(let roleCounter = 0; roleCounter < projectDocuments[counter].userProfile.profileUserTypes.length; roleCounter++){ + Roles = Roles !== "" ? Roles+"," : Roles + Roles += (projectDocuments[counter].userProfile.profileUserTypes[roleCounter].subType ? projectDocuments[counter].userProfile.profileUserTypes[roleCounter].subType.toUpperCase() : projectDocuments[counter].userProfile.profileUserTypes[roleCounter].type.toUpperCase()) + } + userRoleInfomration.Role = Roles + updateProjectDocument["$set"]["userRoleInformation"] = userRoleInfomration + } + + //push all updated and deleted id in arrays and save in file + updatedProjectIds.push(projectDocuments[counter]._id) + deletedSolutionIds.push(projectDocuments[counter].solutionId) + deletedProgramIds.push(projectDocuments[counter].programId) + + // update project documents + await db.collection('projects').findOneAndUpdate({ + "_id" : projectDocuments[counter]._id + },updateProjectDocument); + + await db.collection('solutions').deleteOne({ + _id: projectDocuments[counter].solutionId + }) + await db.collection('programs').deleteOne({ + _id: projectDocuments[counter].programId + }) + } + } + } + + + + + + + + + //write updated project ids to file + fs.writeFile( + 'updatedProjectIdsAll.json', + + JSON.stringify({updatedProjectIds: updatedProjectIds,deletedProgramIds: deletedProgramIds,deletedSolutionIds: deletedSolutionIds}), + + function (err) { + if (err) { + console.error('Crap happens'); + } + } + ); + } + console.log("Updated Project Count : ", updatedProjectIds.length) + console.log("deleted program Count : ", deletedProgramIds.length) + console.log("deleted solutionId Count : ", deletedSolutionIds.length) + console.log("completed") + connection.close(); + } + catch (error) { + console.log(error) + } +})().catch(err => console.error(err)); \ No newline at end of file diff --git a/migrations/updateProjectDocuments/removeAttachmentsNotUploadedInCloud.js b/migrations/updateProjectDocuments/removeAttachmentsNotUploadedInCloud.js new file mode 100644 index 00000000..b0c29f74 --- /dev/null +++ b/migrations/updateProjectDocuments/removeAttachmentsNotUploadedInCloud.js @@ -0,0 +1,210 @@ +/** + * name : updatePrivateProgramInProject.js + * author : Ankit Shahu + * created-date : 02-Feb-2023 + * Description : Migration script for update project + */ + "use strict"; +const path = require("path"); +let rootPath = path.join(__dirname, '../../') +require('dotenv').config({ path: rootPath+'/.env' }) + +let _ = require("lodash"); +let mongoUrl = process.env.MONGODB_URL; +let dbName = mongoUrl.split("/").pop(); +let url = mongoUrl.split(dbName)[0]; +var MongoClient = require('mongodb').MongoClient; +var ObjectId = require('mongodb').ObjectID; +var request = require('request'); +var fs = require('fs'); +const { at } = require("lodash"); + +const filePathUrl = "https://samikshaprod.blob.core.windows.net/samiksha/"; + + +(async () => { + + let connection = await MongoClient.connect(url, { useNewUrlParser: true }); + let db = connection.db(dbName); + try { + // check project attachments for isUploaded = false data and remove the attachment object + let collectionDocs = await db.collection('projects').find({ + "$or": [ + { + "attachments.isUploaded": false + }, + { + "tasks.attachments.isUploaded": false + } + ] + }).project({_id:1}).toArray(); + + //varibale to store all projectIds which are updated + let projectIds = []; + collectionDocs.forEach( eachDoc => { + projectIds.push(eachDoc._id); + }) + + let chunkOfProjectIds = _.chunk(projectIds, 10); + let UpdatedProjectId = [] + //loop project chunks + for ( let chunkPointer = 0; chunkPointer < chunkOfProjectIds.length; chunkPointer++ ) { + + //chunk of project ids + let projectId = chunkOfProjectIds[chunkPointer]; + + //loop for chunk of project id in chunk + for ( let projectIdpointer = 0 ; projectIdpointer < projectId.length; projectIdpointer++ ) { + let id = projectId[projectIdpointer]; + + //pull project Data from DB + let pullProjectDataDB = await db.collection('projects').find({ + _id: id + }).project({}).toArray({}) + + //store in varibale to avoid using [0] + const pullProjectData = pullProjectDataDB[0] + + const allTasksBeforeValidation = JSON.stringify(pullProjectData.tasks) + let allAttachmentsBeforeValidation = "" + + //update Object + let updateObject = { + "$set" : {} + } + + //check if project Document has attachments or not if present then checks length of attachment it should be greater than 0 + if( pullProjectData.hasOwnProperty('attachments') && pullProjectData.attachments.length > 0 ) { + allAttachmentsBeforeValidation = JSON.stringify(pullProjectData.attachments) + //assign attachmentPresent object to update variable + updateObject['$set']['attachments'] = await validatedAttachments(pullProjectData.attachments) + } + + //check if project has tasks available and length of tasks array should be greater than 0 + if( pullProjectData.hasOwnProperty('tasks') && pullProjectData.tasks.length > 0){ + + let tasks = pullProjectData.tasks + updateObject['$set']['tasks'] = [...tasks] + + //for loop for each task + for(let taskCounter = 0; taskCounter< tasks.length; taskCounter++){ + + //checks if task object has key attachment present or not + if(tasks[taskCounter].hasOwnProperty("attachments") && tasks[taskCounter].attachments.length>0){ + //assign attachmentPresent array to task attachments + updateObject['$set']['tasks'][taskCounter].attachments = await validatedAttachments((updateObject['$set']['tasks'][taskCounter].attachments)) + } + } + } + + //push project id which is validated + if(JSON.stringify(updateObject["$set"]["tasks"]) != allTasksBeforeValidation || JSON.stringify(updateObject["$set"]["attachments"]) != allAttachmentsBeforeValidation){ + + if(JSON.stringify(updateObject["$set"]["tasks"]) == allTasksBeforeValidation){ + delete updateObject["$set"].tasks + } + if(JSON.stringify(updateObject["$set"]["attachments"]) == allAttachmentsBeforeValidation){ + delete updateObject["$set"].attachments + } + if(updateObject["$set"].hasOwnProperty("attachments") || updateObject["$set"].hasOwnProperty("tasks")){ + UpdatedProjectId.push(id) + //update project with new varibales + await db.collection('projects').updateOne({_id:id},updateObject)} + } + } + + + } + console.log(UpdatedProjectId) + + + + fs.writeFile( + 'updatedProjectWithAttachments.json', + + JSON.stringify({updatedProjectIds: UpdatedProjectId}), + + function (err) { + if (err) { + console.error('Crap happens'); + } + } + ); + + + + async function validatedAttachments(attachments){ + //varibale to store updated attachment objects + let attachmentsPresent = [] + + //for loop for each attachment + for(let attachmentCounter = 0; attachmentCounter < attachments.length; attachmentCounter++){ + + //check if isUploaded key is present or not and isUploaded should be false and checks type of attachment + if(attachments[attachmentCounter].hasOwnProperty("isUploaded") && !attachments[attachmentCounter].isUploaded && attachments[attachmentCounter].type !== "link"){ + + //checks if document is present or not + let documentExist = await getDocumentStatus(attachments[attachmentCounter].sourcePath) + //if present then push object to new array and update isUploaded to true + if(documentExist.success){ + attachments[attachmentCounter].isUploaded = true + attachmentsPresent.push(attachments[attachmentCounter]) + } + }else{ + //if isUploaded is not present then push object to new array and if type is link then also + attachmentsPresent.push(attachments[attachmentCounter]) + } + } + + return attachmentsPresent; + } + + //function to check if document exists + function getDocumentStatus (sourcePath) { + return new Promise(async (resolve, reject) => { + try { + let url = filePathUrl + sourcePath; + const options = { + headers : { + } + }; + request.get(url,options,userReadCallback); + let result = { + success : true + }; + function userReadCallback(err, data) { + if (err) { + result.success = false; + } else { + if( data.statusCode === 200 ) { + result.success = true; + } else { + result.success = false; + } + + } + return resolve(result); + } + setTimeout(function () { + return resolve (result = { + success : false + }); + }, 5000); + + } catch (error) { + return reject(error); + } + }) + } + + + console.log("Updated Projects", UpdatedProjectId.length) + console.log("finished project attachment deletion based on isUploaded:= false, completed...") + connection.close(); + } + catch (error) { + console.log(error) + } +})().catch(err => console.error(err)); + + diff --git a/migrations/userProfileAbsentInProjects/setUserProfileInProjects.js b/migrations/userProfileAbsentInProjects/setUserProfileInProjects.js new file mode 100644 index 00000000..c739d924 --- /dev/null +++ b/migrations/userProfileAbsentInProjects/setUserProfileInProjects.js @@ -0,0 +1,407 @@ +/** + * name : updateUserProfileInProjects.js + * author : Priyanka Pradeep + * created-date : 10-Nov-2022 + * Description : Migration script for update userProfile in project + */ + + const path = require("path"); + let rootPath = path.join(__dirname, '../../') + require('dotenv').config({ path: rootPath+'/.env' }) + + let _ = require("lodash"); + let mongoUrl = process.env.MONGODB_URL; + let dbName = mongoUrl.split("/").pop(); + let url = mongoUrl.split(dbName)[0]; + var MongoClient = require('mongodb').MongoClient; + var ObjectId = require('mongodb').ObjectID; + + var fs = require('fs'); + const request = require('request'); + + const userServiceUrl = "http://learner-service:9000"; + const endPoint = "/v1/location/search"; + const userReadEndpoint = "/private/user/v1/read"; + +(async () => { + + let connection = await MongoClient.connect(url, { useNewUrlParser: true }); + let db = connection.db(dbName); + try { + + let updatedProjectIds = []; + + //get all projects id where user profile is not there. + let projectDocument = await db.collection('projects').find({ + userRoleInformation: {$exists : true}, + userProfile: {$exists : false}, + }).project({ "_id": 1}).toArray(); + + + let chunkOfProjectDocument = _.chunk(projectDocument, 10); + let projectIds; + + for (let pointerToProject = 0; pointerToProject < chunkOfProjectDocument.length; pointerToProject++) { + + projectIds = await chunkOfProjectDocument[pointerToProject].map( + projectDoc => { + return projectDoc._id; + } + ); + + //get user ids of project without profile + let userIdsWithoutProfile = await db.collection('projects').find({ + _id: { $in : projectIds } + }).project({ + "_id" : 1, + "userId" : 1 + }).toArray(); + + //loop userIds- These user profiles are absent in project + for ( let count = 0; count < userIdsWithoutProfile.length; count++ ) { + + let projectIdWithoutUserProfile = userIdsWithoutProfile[count]._id; + let userId = userIdsWithoutProfile[count].userId; + + //call profile api to get user profile + let profile = await profileReadPrivate(userId); + + // update project with profile + if( profile.success && profile.data && profile.data.response ) { + let updateObject = { + "$set" : {} + }; + updateObject["$set"]["userProfile"] = profile.data.response; + + await db.collection('projects').findOneAndUpdate({ + "_id" : projectIdWithoutUserProfile + },updateObject); + } + + } + + + let projectDocuments = await db.collection('projects').find({ + _id: { $in : projectIds}, + userProfile: {$exists : true} + }).project({ + "_id": 1, + "userRoleInformation" : 1, + "userProfile" : 1 + }).toArray(); + + //loop all projects + for ( let count = 0; count < projectDocuments.length; count++ ) { + + let project = projectDocuments[count]; + let userProfile = project.userProfile; + + + let updateUserProfileRoleInformation = false; // Flag to see if roleInformation i.e. userProfile.profileUserTypes has to be updated based on userRoleInfromation.roles + + if(project.userRoleInformation.role) { // Check if userRoleInformation has role value. + let rolesInUserRoleInformation = project.userRoleInformation.role.split(","); // userRoleInfomration.role can be multiple with comma separated. + + let resetCurrentUserProfileRoles = false; // Flag to reset current userProfile.profileUserTypes i.e. if current role in profile is not at all there in userRoleInformation.roles + // Check if userProfile.profileUserTypes exists and is an array of length > 0 + if(userProfile.profileUserTypes && Array.isArray(userProfile.profileUserTypes) && userProfile.profileUserTypes.length >0) { + + // Loop through current roles in userProfile.profileUserTypes + for (let pointerToCurrentProfileUserTypes = 0; pointerToCurrentProfileUserTypes < userProfile.profileUserTypes.length; pointerToCurrentProfileUserTypes++) { + const currentProfileUserType = userProfile.profileUserTypes[pointerToCurrentProfileUserTypes]; + + if(currentProfileUserType.subType && currentProfileUserType.subType !== null) { // If the role has a subType + + // Check if subType exists in userRoleInformation role, if not means profile data is old and should be reset. + if(!project.userRoleInformation.role.toUpperCase().includes(currentProfileUserType.subType.toUpperCase())) { + resetCurrentUserProfileRoles = true; // Reset userProfile.profileUserTypes + break; + } + } else { // If the role subType is null or is not there + + // Check if type exists in userRoleInformation role, if not means profile data is old and should be reset. + if(!project.userRoleInformation.role.toUpperCase().includes(currentProfileUserType.type.toUpperCase())) { + resetCurrentUserProfileRoles = true; // Reset userProfile.profileUserTypes + break; + } + } + } + } + if(resetCurrentUserProfileRoles) { // Reset userProfile.profileUserTypes + userProfile.profileUserTypes = new Array; + } + + // Loop through each subRole in userRoleInformation + for (let pointerToRolesInUserInformation = 0; pointerToRolesInUserInformation < rolesInUserRoleInformation.length; pointerToRolesInUserInformation++) { + const subRole = rolesInUserRoleInformation[pointerToRolesInUserInformation]; + + // Check if userProfile.profileUserTypes exists and is an array of length > 0 + if(userProfile.profileUserTypes && Array.isArray(userProfile.profileUserTypes) && userProfile.profileUserTypes.length >0) { + if(!_.find(userProfile.profileUserTypes, { 'type': subRole.toLowerCase() }) && !_.find(userProfile.profileUserTypes, { 'subType': subRole.toLowerCase() })) { + updateUserProfileRoleInformation = true; // Need to update userProfile.profileUserTypes + if(subRole.toUpperCase() === "TEACHER") { // If subRole is not teacher + userProfile.profileUserTypes.push({ + "subType" : null, + "type" : "teacher" + }) + } else { // If subRole is not teacher + userProfile.profileUserTypes.push({ + "subType" : subRole.toLowerCase(), + "type" : "administrator" + }) + } + } + } else { // Make a new entry if userProfile.profileUserTypes is empty or does not exist. + updateUserProfileRoleInformation = true; // Need to update userProfile.profileUserTypes + userProfile.profileUserTypes = new Array; + if(subRole.toUpperCase() === "TEACHER") { // If subRole is teacher + userProfile.profileUserTypes.push({ + "subType" : null, + "type" : "teacher" + }) + } else { // If subRole is not teacher + userProfile.profileUserTypes.push({ + "subType" : subRole.toLowerCase(), + "type" : "administrator" + }) + } + } + } + } + + // Create location only object from userRoleInformation + let userRoleInformationLocationObject = _.omit(project.userRoleInformation, ['role']); + + // All location keys from userRoleInformation + let userRoleInfomrationLocationKeys = Object.keys(userRoleInformationLocationObject); + + let updateUserProfileLocationInformation = false; // Flag to see if userLocations i.e. userProfile.userLocations has to be updated based on userRoleInfromation location values + + // Loop through all location keys. + for (let pointerToUserRoleInfromationLocationKeys = 0; pointerToUserRoleInfromationLocationKeys < userRoleInfomrationLocationKeys.length; pointerToUserRoleInfromationLocationKeys++) { + + const locationType = userRoleInfomrationLocationKeys[pointerToUserRoleInfromationLocationKeys]; // e.g. state, district, school + const locationValue = userRoleInformationLocationObject[locationType]; // Location UUID values or school code. + + // Check if userProfile.userLocations exists and is an array of length > 0 + if(userProfile.userLocations && Array.isArray(userProfile.userLocations) && userProfile.userLocations.length >0) { + + if(locationType === "school") { // If location type school exist check if same is there in userProfile.userLocations + if(!_.find(userProfile.userLocations, { 'type': "school", 'code': locationValue })) { + updateUserProfileLocationInformation = true; // School does not exist in userProfile.userLocations, update entire userProfile.userLocations + break; + } + } else { // Check if location type is there in userProfile.userLocations and has same value as userRoleInformation + if(!_.find(userProfile.userLocations, { 'type': locationType, 'id': locationValue })) { + updateUserProfileLocationInformation = true; // Location does not exist in userProfile.userLocations, update entire userProfile.userLocations + break; + } + } + } else { + updateUserProfileLocationInformation = true; + break; + } + } + + + if(userProfile.userLocations && Array.isArray(userProfile.userLocations) && userProfile.userLocations.length >0) { + if(userProfile.userLocations.length != userRoleInfomrationLocationKeys.length) { + updateUserProfileLocationInformation = true; + } + } + + // If userProfile.userLocations has to be updated, get all values and set in userProfile. + if(updateUserProfileLocationInformation) { + + //update userLocations in userProfile + let locationIds = []; + let locationCodes = []; + let userLocations = new Array; + + userRoleInfomrationLocationKeys.forEach( requestedDataKey => { + if (checkIfValidUUID(userRoleInformationLocationObject[requestedDataKey])) { + locationIds.push(userRoleInformationLocationObject[requestedDataKey]); + } else { + locationCodes.push(userRoleInformationLocationObject[requestedDataKey]); + } + }) + + //query for fetch location using id + if ( locationIds.length > 0 ) { + let locationQuery = { + "id" : locationIds + } + + let entityData = await locationSearch(locationQuery); + if ( entityData.success ) { + userLocations = entityData.data; + } + } + + // query for fetch location using code + if ( locationCodes.length > 0 ) { + let codeQuery = { + "code" : locationCodes + } + + let entityData = await locationSearch(codeQuery); + if ( entityData.success ) { + userLocations = userLocations.concat(entityData.data); + } + } + + if ( userLocations.length > 0 ) { + userProfile["userLocations"] = userLocations; + } + } + + + //update projects if userProfile role or location information is incorrect + if ( updateUserProfileRoleInformation || updateUserProfileLocationInformation ) { + + let updateObject = { + "$set" : {} + }; + if(updateUserProfileRoleInformation) { + updateObject["$set"]["userProfile.profileUserTypes"] = userProfile.profileUserTypes; + updateObject["$set"]["userProfile.userRoleMismatchFoundAndUpdated"] = true; + } + if(updateUserProfileLocationInformation) { + updateObject["$set"]["userProfile.userLocations"] = userProfile.userLocations; + updateObject["$set"]["userProfile.userLocationsMismatchFoundAndUpdated"] = true; + } + + await db.collection('projects').findOneAndUpdate({ + "_id" : project._id + },updateObject); + + updatedProjectIds.push(project._id.toString()); + } + + } + + //write updated project ids to file + fs.writeFile( + 'updatedProjectIds.json', + + JSON.stringify(updatedProjectIds), + + function (err) { + if (err) { + console.error('Crap happens'); + } + } + ); + } + + function locationSearch ( filterData ) { + return new Promise(async (resolve, reject) => { + try { + + let bodyData={}; + bodyData["request"] = {}; + bodyData["request"]["filters"] = filterData; + const url = userServiceUrl + endPoint; + const options = { + headers : { + "content-type": "application/json" + }, + json : bodyData + }; + + request.post(url,options,requestCallback); + + let result = { + success : true + }; + + function requestCallback(err, data) { + if (err) { + result.success = false; + } else { + let response = data.body; + if( response.responseCode === "OK" && + response.result && + response.result.response && + response.result.response.length > 0 + ) { + let entityResult = new Array; + response.result.response.map(entityData => { + let entity = _.omit(entityData, ['identifier']); + entityResult.push(entity); + }) + result["data"] = entityResult; + result["count"] = response.result.count; + } else { + result.success = false; + } + } + return resolve(result); + } + + setTimeout(function () { + return resolve (result = { + success : false + }); + }, 5000); + + } catch (error) { + return reject(error); + } + }) + } + + function profileReadPrivate (userId) { + return new Promise(async (resolve, reject) => { + try { + // <--- Important : This url endpoint is private do not use it for regular workflows ---> + let url = userServiceUrl + userReadEndpoint + "/" + userId; + const options = { + headers : { + "content-type": "application/json" + } + }; + request.get(url,options,userReadCallback); + let result = { + success : true + }; + function userReadCallback(err, data) { + if (err) { + result.success = false; + } else { + + let response = JSON.parse(data.body); + if( response.responseCode === "OK" ) { + result["data"] = response.result; + } else { + result.success = false; + } + + } + return resolve(result); + } + setTimeout(function () { + return resolve (result = { + success : false + }); + }, 5000); + + } catch (error) { + return reject(error); + } + }) + } + + console.log("Updated Project Count : ", updatedProjectIds.length) + console.log("completed") + connection.close(); + } + catch (error) { + console.log(error) + } +})().catch(err => console.error(err)); + +function checkIfValidUUID(value) { + const regexExp = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi; + return regexExp.test(value); +} \ No newline at end of file diff --git a/models/certificateTemplates.js b/models/certificateTemplates.js new file mode 100644 index 00000000..0f5ba2d7 --- /dev/null +++ b/models/certificateTemplates.js @@ -0,0 +1,29 @@ +module.exports = { + name: "certificateTemplates", + schema: { + templateUrl: String, + issuer: { + type : Object, + required : true + }, + status: { + type : String, + required : true, + default : "ACTIVE" + }, + solutionId: { + type : "ObjectId", + index : true, + unique : true + }, + programId: { + type : "ObjectId", + index : true, + required : true + }, + criteria: { + type : Object, + required : true + } + } +}; \ No newline at end of file diff --git a/models/programs.js b/models/programs.js index cb411bb3..4aaa6262 100644 --- a/models/programs.js +++ b/models/programs.js @@ -17,6 +17,14 @@ module.exports = { type : String, index : true }, + startDate:{ + type: Date, + index: true + }, + endDate: { + type : Date, + index : true + }, resourceType: [String], language: [String], keywords: [String], diff --git a/models/project-templates.js b/models/project-templates.js index 4fddac32..e1df6b92 100644 --- a/models/project-templates.js +++ b/models/project-templates.js @@ -112,6 +112,7 @@ module.exports = { 4 : 0, 5 : 0 } - } + }, + certificateTemplateId : "ObjectId" } }; \ No newline at end of file diff --git a/models/projects.js b/models/projects.js index a15aa3c7..f6f5bb61 100644 --- a/models/projects.js +++ b/models/projects.js @@ -142,7 +142,33 @@ module.exports = { default : [] }, remarks : String, - userProfile : Object + userProfile : Object, + certificate : { + templateId : "ObjectId", + osid : { + type : String, + index : true, + unique : true + }, + transactionId : { + type : String, + index : true, + unique : true + }, + templateUrl : String, + status : String, + eligible : Boolean, + message : String, + issuedOn : Date, + criteria : Object, + reIssuedAt : Date, + transactionIdCreatedAt : Date, + originalTransactionInformation :{ + transactionId : String, + osid : String + } + + } }, compoundIndex: [ { diff --git a/models/solutions.js b/models/solutions.js index c55a0508..536c67c3 100644 --- a/models/solutions.js +++ b/models/solutions.js @@ -96,6 +96,9 @@ module.exports = { type: Number, default: 1 }, - reportInformation : Object + reportInformation : Object, + certificateTemplateId : "ObjectId", + rootOrganisations : Array, + createdFor : Array } }; \ No newline at end of file diff --git a/module/certificateValidations/helper.js b/module/certificateValidations/helper.js new file mode 100644 index 00000000..82a591cb --- /dev/null +++ b/module/certificateValidations/helper.js @@ -0,0 +1,233 @@ +/** + * name : helper.js + * author : vishnu + * created-date : 26-Oct-2022 + * Description : certificate validation helper functionality. + */ + +// Dependencies + +/** + * certificateValidationsHelper + * @class +*/ + +module.exports = class certificateValidationsHelper { + + /** + * validate certificate criteria. + * @method + * @name criteriaValidation + * @param {Object} data - project data for certificate creation + * @returns + */ + + static criteriaValidation(data) { + return new Promise(async (resolve, reject) => { + try { + let criteria = data.certificate.criteria; // criteria conditions for certificate + let validationResult = []; + let validationMessage = ""; + let validationExpression = criteria.expression + if ( criteria.conditions && Object.keys(criteria.conditions).length > 0 ) { + let conditions = criteria.conditions; + let conditionKeys = Object.keys(conditions) + + for ( let index = 0; index < conditionKeys.length; index++ ) { + // correntCondition contain the prefinal level data + let currentCondition = conditions[conditionKeys[index]]; + + //now pass expression and validation scope to another function which will start the validation procedure + let validation = await _subCriteriaValidation( currentCondition.conditions, currentCondition.expression, data ); + + validationResult.push(validation.success); + ( validation.success == false ) ? validationMessage = validationMessage + " " + currentCondition.validationText : ""; + } + // validate criteria using defined expression + let criteriaValidation = await _criteriaExpressionValidation( validationExpression, conditionKeys, validationResult ) + return resolve({ + success: criteriaValidation, + message: ( criteriaValidation == false ) ? validationMessage : CONSTANTS.common.PROJECT_CERTIFICATE_GENERATED_SUCCESSFULLY + }); + } + return resolve({ + success: false + }) + } catch (error) { + return resolve({ + success: false, + message: error.message, + data: {} + }); + } + }) + } +}; + +/** + * _subCriteriaValidation. + * @method + * @name _subCriteriaValidation + * @param {Object} conditions - condition data. + * @param {String} expression - validation expression + * @returns {Boolean} validation. +*/ + +function _subCriteriaValidation(conditions, expression, data) { + return new Promise(async (resolve, reject) => { + try { + let conditionKeys = Object.keys(conditions) + let validationResult = []; + // loop throug conditions of subcriterias + for ( let index = 0; index < conditionKeys.length; index++ ) { + let currentCondition = conditions[conditionKeys[index]]; + // correntCondition contain the prefinal level data + //now pass expression and validation scope to another function which will start the validation procedure + let validation = await _validateCriteriaConditions( currentCondition, data ); + validationResult.push(validation); + } + // validate expression + let subcriteriaValidation = await _criteriaExpressionValidation( expression, conditionKeys, validationResult ) + return resolve({ + success: subcriteriaValidation + }); + + } catch (error) { + return resolve({ + message: error.message, + success: false, + status: + error.status ? + error.status : HTTP_STATUS_CODE['internal_server_error'].status + }) + } + }) +} + +/** + * _validateCriteriaConditions. + * @method + * @name _validateCriteriaConditions + * @param {Object} condition - condition data. + * @param {String} data - validation data + * @returns {Boolean} validation. +*/ + +function _validateCriteriaConditions(condition, data) { + return new Promise(async (resolve, reject) => { + try { + let result = false; + if ( !condition.function || condition.function == "" ) { + if ( condition.scope == CONSTANTS.common.PROJECT ){ + // if validation is on completedDate + if ( condition.key == "completedDate") { + let comparableDates = UTILS.createComparableDates( data[condition.key], condition.value ); + data[condition.key] = comparableDates.dateOne; + condition.value = comparableDates.dateTwo; + } + // validate prject value with condition value + result = UTILS.operatorValidation( data[condition.key], condition.value, condition.operator ); + + } + } else { + try { + let valueFromProject = 0; + // if: condition is in scope of project and contains a function to check + if ( condition.scope == CONSTANTS.common.PROJECT ) { + // get count of attachments at project level + valueFromProject = UTILS.noOfElementsInArray( data[condition.key], condition.filter ); + } else if ( condition.scope == CONSTANTS.common.TASK_ATTACHMENT ){ + // for task attachment validatiion _id of specific task or "all" key should be passed in an array called taskDetails + let tasksAttachments = []; + let projectTasks = data.tasks; + // check tasks and taskDetails exists + if ( projectTasks && projectTasks.length > 0 && condition.taskDetails.length > 0 && condition.taskDetails[0] == "all" ) { + // loop through tasks to get attachments + for ( let tasksIndex = 0; tasksIndex < projectTasks.length; tasksIndex++ ) { + + if ( projectTasks[tasksIndex][condition.key] && projectTasks[tasksIndex][condition.key].length > 0 ) + { + tasksAttachments.push(...projectTasks[tasksIndex][condition.key]) + } + } + + } else if ( projectTasks && projectTasks.length > 0 && condition.taskDetails.length > 0 ) { + + // specific task Id( from projectTemplates ) or Ids are passed for attachment validation + for ( let tasksIndex = 0; tasksIndex < projectTasks.length; tasksIndex++ ) { + for ( let taskDetailsPointer = 0; taskDetailsPointer < condition.taskDetails.length; taskDetailsPointer++ ) { + // get attachments data of specified task/ tasks + if ( projectTasks[tasksIndex].referenceId == condition.taskDetails[taskDetailsPointer] && projectTasks[tasksIndex][condition.key] && projectTasks[tasksIndex][condition.key].length > 0 ) { + tasksAttachments.push(...projectTasks[tasksIndex][condition.key]) + } + } + + } + + } else { + return resolve(result) + } + if ( !tasksAttachments.length > 0 ) { + return resolve(result) + } + // get task attachments count + valueFromProject = UTILS.noOfElementsInArray( tasksAttachments, condition.filter ); + } + // validate against condition value + result = UTILS.operatorValidation( valueFromProject, condition.value, condition.operator ); + + } catch (fnError) { + return resolve(result) + } + } + return resolve(result); + } catch (error) { + return resolve({ + message: error.message, + success: false, + status: + error.status ? + error.status : HTTP_STATUS_CODE['internal_server_error'].status + }) + } + }) +} +/** + * _criteriaExpressionValidation + * @method + * @name _criteriaExpressionValidation + * @param {String} expression - criteria expression + * @param {Array} keys - condition keys + * @param {Array} result - condition result + * @returns {Boolean} validation result. +*/ + +function _criteriaExpressionValidation(expression, keys, result) { + return new Promise(async (resolve, reject) => { + try { + + if( expression == "" || + !keys.length > 0 || + !result.length > 0 || + keys.length != result.length ) { + return resolve(false); + } + // generate expression string that can be evaluated + for ( let pointerToKeys = 0; pointerToKeys < keys.length; pointerToKeys++ ) { + expression = expression.replace(keys[pointerToKeys],result[pointerToKeys].toString()) + } + let evalResult = eval(expression) + + return resolve(evalResult); + + } catch (error) { + return resolve(false); + } + }) +} + + + + + + diff --git a/module/library/categories/helper.js b/module/library/categories/helper.js index 67828b8d..042761cc 100644 --- a/module/library/categories/helper.js +++ b/module/library/categories/helper.js @@ -198,7 +198,7 @@ module.exports = class LibraryCategoriesHelper { message : CONSTANTS.apiResponses.PROJECT_NOT_FOUND, }; } - + projectsData[0].showProgramAndEntity = false; if( projectsData[0].tasks && projectsData[0].tasks.length > 0 ) { @@ -259,7 +259,7 @@ module.exports = class LibraryCategoriesHelper { data : projectsData[0] }); - } catch (error) { + } catch (error) { return resolve({ status : error.status ? error.status : HTTP_STATUS_CODE['internal_server_error'].status, success: false, diff --git a/module/project/templates/helper.js b/module/project/templates/helper.js index 8b07d62d..6e3404f8 100644 --- a/module/project/templates/helper.js +++ b/module/project/templates/helper.js @@ -22,7 +22,7 @@ const projectTemplateTaskQueries = require(DB_QUERY_BASE_PATH + "/projectTemplat const projectQueries = require(DB_QUERY_BASE_PATH + "/projects"); const projectCategoriesQueries = require(DB_QUERY_BASE_PATH + "/projectCategories"); const solutionsQueries = require(DB_QUERY_BASE_PATH + "/solutions"); - +const certificateTemplateQueries = require(DB_QUERY_BASE_PATH + "/certificateTemplates"); module.exports = class ProjectTemplatesHelper { @@ -557,7 +557,7 @@ module.exports = class ProjectTemplatesHelper { externalId : templateId, isReusable : true }); - + if ( !projectTemplateData.length > 0 ) { throw new Error(CONSTANTS.apiResponses.PROJECT_TEMPLATE_NOT_FOUND) } @@ -1048,6 +1048,19 @@ module.exports = class ProjectTemplatesHelper { message :CONSTANTS.apiResponses.PROJECT_TEMPLATE_NOT_FOUND } } + if ( templateData[0].certificateTemplateId && templateData[0].certificateTemplateId !== "" ){ + let certificateTemplateDetails = await certificateTemplateQueries.certificateTemplateDocument({ + _id : templateData[0].certificateTemplateId + },["criteria"]); + + //certificate template data do not exists. + if ( !certificateTemplateDetails.length > 0 ) { + throw { + message: CONSTANTS.apiResponses.CERTIFICATE_TEMPLATE_NOT_FOUND + }; + } + templateData[0].criteria = certificateTemplateDetails[0].criteria + } if (templateData[0].tasks && templateData[0].tasks.length > 0) { templateData[0].tasks = diff --git a/module/reports/helper.js b/module/reports/helper.js index a07f2ec5..c1d4c6b3 100644 --- a/module/reports/helper.js +++ b/module/reports/helper.js @@ -361,12 +361,12 @@ module.exports = class ReportsHelper { } if (userRole != "") { + let regex = userRole.split(","); + regex.push(""); query.userRole = { - $in : [ - "", - ...userRole.split(",") - ] - } + $regex: regex.join("|"), + $options: "i", + }; } let searchQuery = []; diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index e7da3e37..67d487fc 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -23,6 +23,10 @@ const removeFieldsFromRequest = ["submissionDetails"]; const programsQueries = require(DB_QUERY_BASE_PATH + "/programs"); const userProfileService = require(GENERICS_FILES_PATH + "/services/users"); const solutionsHelper = require(MODULES_BASE_PATH + "/solutions/helper"); +const certificateTemplateQueries = require(DB_QUERY_BASE_PATH + "/certificateTemplates"); +const certificateService = require(GENERICS_FILES_PATH + "/services/certificate"); +const certificateValidationsHelper = require(MODULES_BASE_PATH + "/certificateValidations/helper"); +const _ = require("lodash"); /** * UserProjectsHelper @@ -142,9 +146,11 @@ module.exports = class UserProjectsHelper { const projectsModel = Object.keys(schemas["projects"].schema); - if(data.userRoleInformation) delete data.userRoleInformation; - if(data.userProfile) delete data.userProfile; - + let keysToRemoveFromUpdation = ["userRoleInformation","userProfile","certificate"] + keysToRemoveFromUpdation.forEach( key => { + if (data[key])delete data[key]; + }) + let updateProject = {}; let projectData = await _projectData(data); if (projectData && projectData.success == true) { @@ -355,7 +361,7 @@ module.exports = class UserProjectsHelper { if ( data.status == CONSTANTS.common.COMPLETED_STATUS || data.status == CONSTANTS.common.SUBMITTED_STATUS ) { updateProject.completedDate = new Date(); } - + let projectUpdated = await projectQueries.findOneAndUpdate( { @@ -374,9 +380,10 @@ module.exports = class UserProjectsHelper { status: HTTP_STATUS_CODE['bad_request'].status } } - + + // push project details to kafka await kafkaProducersHelper.pushProjectToKafka(projectUpdated); - + return resolve({ success: true, message: CONSTANTS.apiResponses.USER_PROJECT_UPDATED, @@ -457,9 +464,9 @@ module.exports = class UserProjectsHelper { result.solutionInformation = _.pick( solutionAndProgramCreation.data.solution, - ["name", "externalId", "description", "_id", "entityType"] + ["name", "externalId", "description", "_id", "entityType", "certificateTemplateId"] ); - + result.solutionInformation._id = ObjectId(result.solutionInformation._id); @@ -1162,7 +1169,20 @@ module.exports = class UserProjectsHelper { if( appVersion !== "" ) { projectCreation.data["appInformation"]["appVersion"] = appVersion; } - + + if ( solutionDetails.certificateTemplateId && solutionDetails.certificateTemplateId !== "" ) { + // <- Add certificate template details to projectCreation data if present -> + const certificateTemplateDetails = await certificateTemplateQueries.certificateTemplateDocument({ + _id : solutionDetails.certificateTemplateId + }); + + // create certificate object and add data if certificate template is present. + if ( certificateTemplateDetails.length > 0 ) { + projectCreation.data["certificate"] = _.pick(certificateTemplateDetails[0], ['templateUrl', 'status', 'criteria']); + projectCreation.data["certificate"]["templateId"] = solutionDetails.certificateTemplateId; + } + } + let getUserProfileFromObservation = false; if( bodyData && Object.keys(bodyData).length > 0 ) { @@ -1253,7 +1273,12 @@ module.exports = class UserProjectsHelper { ) { projectCreation.data.userProfile = userProfile.data.response; addReportInfoToSolution = true; - } + } else { + throw { + message: CONSTANTS.apiResponses.FAILED_TO_START_RESOURCE, + status: HTTP_STATUS_CODE["failed_dependency"].status, + }; + } } } else { @@ -1266,7 +1291,12 @@ module.exports = class UserProjectsHelper { ) { projectCreation.data.userProfile = userProfileData.data.response; addReportInfoToSolution = true; - } + } else { + throw { + message: CONSTANTS.apiResponses.FAILED_TO_START_RESOURCE, + status: HTTP_STATUS_CODE["failed_dependency"].status, + }; + } } projectCreation.data.userRoleInformation = userRoleInformation; @@ -1310,6 +1340,21 @@ module.exports = class UserProjectsHelper { } else { projectDetails.data.status = UTILS.convertProjectStatus(projectDetails.data.status); } + // make templateUrl downloadable befor passing to front-end + if ( projectDetails.data.certificate && + projectDetails.data.certificate.templateUrl && + projectDetails.data.certificate.templateUrl !== "" + ) { + let certificateTemplateDownloadableUrl = + await coreService.getDownloadableUrl( + { + filePaths: [projectDetails.data.certificate.templateUrl] + } + ); + if ( certificateTemplateDownloadableUrl.success ) { + projectDetails.data.certificate.templateUrl = certificateTemplateDownloadableUrl.data[0].url; + } + } return resolve({ success: true, @@ -1343,7 +1388,6 @@ module.exports = class UserProjectsHelper { static userAssignedProjectCreation(templateId, userId, userToken) { return new Promise(async (resolve, reject) => { try { - const projectTemplateData = await projectTemplateQueries.templateDocument({ status: CONSTANTS.common.PUBLISHED, @@ -1382,7 +1426,7 @@ module.exports = class UserProjectsHelper { await projectTemplatesHelper.tasksAndSubTasks( projectTemplateData[0]._id ); - + if (tasksAndSubTasks.length > 0) { result.tasks = _projectTask(tasksAndSubTasks); @@ -1894,7 +1938,7 @@ module.exports = class UserProjectsHelper { query["referenceFrom"] = CONSTANTS.common.LINK; } } - + let projects = await this.projects( query, pageSize, @@ -1911,10 +1955,11 @@ module.exports = class UserProjectsHelper { "lastDownloadedAt", "hasAcceptedTAndC", "referenceFrom", - "status" + "status", + "certificate" ] ); - + let totalCount = 0; let data = []; @@ -1922,9 +1967,11 @@ module.exports = class UserProjectsHelper { totalCount = projects.data.count; data = projects.data.data; - + if( data.length > 0 ) { + let templateFilePath = []; data.forEach( projectData => { + projectData.name = projectData.title; @@ -1940,7 +1987,44 @@ module.exports = class UserProjectsHelper { projectData.type = CONSTANTS.common.IMPROVEMENT_PROJECT; delete projectData.title; + + if (projectData.certificate && + projectData.certificate.osid && + projectData.certificate.osid !== "" && + projectData.certificate.templateUrl && + projectData.certificate.templateUrl !== "" + ) { + templateFilePath.push(projectData.certificate.templateUrl); + } + }); + + if( templateFilePath.length > 0 ) { + + let certificateTemplateDownloadableUrl = + await coreService.getDownloadableUrl( + { + filePaths: templateFilePath + } + ); + if ( !certificateTemplateDownloadableUrl.success ) { + throw { + message: CONSTANTS.apiResponses.DOWNLOADABLE_URL_NOT_FOUND + }; + } + // map downloadable templateUrl to corresponding project data + data.forEach(projectData => { + if (projectData.certificate) { + var itemFromUrlArray = certificateTemplateDownloadableUrl.data.find(item=> item.filePath == projectData.certificate.templateUrl); + if (itemFromUrlArray) { + projectData.certificate.templateUrl = itemFromUrlArray.url; + } + } + } + + ) + } + } } @@ -2080,8 +2164,7 @@ module.exports = class UserProjectsHelper { "", isATargetedSolution ); - - + if ( libraryProjects.data && !Object.keys(libraryProjects.data).length > 0 @@ -2193,7 +2276,24 @@ module.exports = class UserProjectsHelper { programAndSolutionInformation.data ) } - + // <- Add certificate template data + if ( + libraryProjects.data.certificateTemplateId && + libraryProjects.data.certificateTemplateId !== "" + ){ + // <- Add certificate template details to projectCreation data if present -> + const certificateTemplateDetails = await certificateTemplateQueries.certificateTemplateDocument({ + _id : libraryProjects.data.certificateTemplateId + }); + + // create certificate object and add data if certificate template is present. + if ( certificateTemplateDetails.length > 0 ) { + libraryProjects.data["certificate"] = _.pick(certificateTemplateDetails[0], ['templateUrl', 'status', 'criteria']); + } + libraryProjects.data["certificate"]["templateId"] = libraryProjects.data.certificateTemplateId; + delete libraryProjects.data.certificateTemplateId; + } + //Fetch user profile information by calling sunbird's user read api. let addReportInfoToSolution = false; let userProfile = await userProfileService.profile(userToken, userId); @@ -2217,13 +2317,17 @@ module.exports = class UserProjectsHelper { libraryProjects.data.endDate = requestedData.endDate; } + if (requestedData.hasAcceptedTAndC) { + libraryProjects.data.hasAcceptedTAndC = true; + } + libraryProjects.data.projectTemplateId = libraryProjects.data._id; libraryProjects.data.projectTemplateExternalId = libraryProjects.data.externalId; - + let projectCreation = await database.models.projects.create( _.omit(libraryProjects.data, ["_id"]) ); - + if ( addReportInfoToSolution && projectCreation._doc.solutionId ) { let updateSolution = await solutionsHelper.addReportInformationInSolution( @@ -2241,9 +2345,9 @@ module.exports = class UserProjectsHelper { userToken ); } - + projectCreation = await _projectInformation(projectCreation._doc); - + return resolve({ success: true, message: CONSTANTS.apiResponses.PROJECTS_FETCHED, @@ -2260,15 +2364,15 @@ module.exports = class UserProjectsHelper { }) } - /** - * get project details. - * @method - * @name userProject - * @param {String} projectId - project id. - * @returns {Object} Project details. - */ + /** + * get project details. + * @method + * @name userProject + * @param {String} projectId - project id. + * @returns {Object} Project details. + */ - static userProject(projectId) { + static userProject(projectId) { return new Promise(async (resolve, reject) => { try { @@ -2300,6 +2404,505 @@ module.exports = class UserProjectsHelper { }) } + /** + * generate project certificate. + * @method + * @name generateCertificate + * @param {Object} data - project data for certificate creation data. + * @returns {JSON} certificate details. + */ + + static generateCertificate(data) { + return new Promise(async (resolve, reject) => { + try { + + // check eligibility of project for certificate creation + let eligibility = await this.checkCertificateEligibility(data); + if (!eligibility ){ + throw { + message: CONSTANTS.apiResponses.NOT_ELIGIBLE_FOR_CERTIFICATE + }; + } + + // create payload for certificate generation + const certificateData = await this.createCertificatePayload(data); + + // call sunbird-RC to create certificate for project + const certificate = await this.createCertificate(certificateData, data._id) + + return resolve(certificate); + + } catch (error) { + return resolve({ + success: false, + message: error.message + + }); + } + }) + } + + /** + * check project eligibility for certificate. + * @method + * @name checkCertificateEligibility + * @param {Object} data - project data for certificate creation data. + * @returns {Boolean} certificate eligibilty status. + */ + + static checkCertificateEligibility(data) { + return new Promise(async (resolve, reject) => { + try { + let eligible = false; + let updateObject = { + "$set" : {} + }; + // validate certificate data, checking if it passes all criteria + let validateCriteria = await certificateValidationsHelper.criteriaValidation(data) + if ( validateCriteria.success ) { + eligible = true; + } else { + updateObject["$set"]["certificate.message"] = validateCriteria.message; + } + updateObject["$set"]["certificate.eligible"] = eligible; + + // update project certificate data + await projectQueries.findOneAndUpdate( + { + _id: data._id + }, + updateObject + ); + + return resolve(eligible); + } catch (error) { + return resolve({ + success: false, + message: error.message + + }); + } + }) + } + + /** + * createCertificatePayload. + * @method + * @name createCertificatePayload + * @param {Object} data - project data for certificate creation data. + * @returns {Object} payload for certificate creation. + */ + + static createCertificatePayload(data) { + return new Promise(async (resolve, reject) => { + try { + console.log("Certificate issuer Kid: ",CERTIFICATE_ISSUER_KID) + + if(data.title.length > 75) { + data.title = data.title.substring(0, 75) + '...'; + } + + // get downloadable url for certificate template + if ( data.certificate.templateUrl && data.certificate.templateUrl !== "" ) { + let certificateTemplateDownloadableUrl = + await coreService.getDownloadableUrl( + { + filePaths: [data.certificate.templateUrl] + } + ); + if ( certificateTemplateDownloadableUrl.success ) { + data.certificate.templateUrl = certificateTemplateDownloadableUrl.data[0].url; + } else { + throw { + message: CONSTANTS.apiResponses.DOWNLOADABLE_URL_NOT_FOUND + }; + } + } + + let certificateTemplateDetails =[]; + if ( data.certificate.templateId && data.certificate.templateId !== "" ) { + certificateTemplateDetails = await certificateTemplateQueries.certificateTemplateDocument({ + _id : data.certificate.templateId + },["issuer","solutionId","programId"]); + + //certificate template data do not exists. + if ( !certificateTemplateDetails.length > 0 ) { + throw { + message: CONSTANTS.apiResponses.CERTIFICATE_TEMPLATE_NOT_FOUND + }; + } + certificateTemplateDetails[0].issuer.kid = CERTIFICATE_ISSUER_KID; + } + let certificateUserName; + if (data.userProfile.lastName && data.userProfile.lastName.length > 0) { + certificateUserName = `${data.userProfile.firstName} ${data.userProfile.lastName}`; + } else { + certificateUserName = `${data.userProfile.firstName}`; + } + //create certificate request body + let certificateData = { + recipient : { + id : data.userId, + name : certificateUserName, + type : data.userProfile.profileUserType.type + }, + templateUrl : data.certificate.templateUrl, + issuer : certificateTemplateDetails[0].issuer, + status : data.certificate.status.toUpperCase(), + projectId : (data._id).toString(), + projectName : data.title, + programId : (certificateTemplateDetails[0].programId).toString(), + programName : ( data.programInformation && data.programInformation.name ) ? data.programInformation.name : "", + solutionId : (certificateTemplateDetails[0].solutionId).toString(), + solutionName : ( data.solutionInformation && data.solutionInformation.name ) ? data.solutionInformation.name : "", + completedDate : data.completedDate + }; + return resolve(certificateData); + + } catch (error) { + console.log("error:",error.message) + return resolve({ + success: false, + message: error.message + + }); + } + }) + } + + /** + * call sunbird-RC for certificate creation. + * @method + * @name createCertificate + * @param {Object} certificateData - payload for certificate creation data. + * @param {string} projectId - project Id. + * @returns {Boolean} certificate creation status. + */ + + static createCertificate(certificateData, projectId) { + return new Promise(async (resolve, reject) => { + try { + + const certificateDetails = await certificateService.createCertificate( certificateData ); + + if ( !certificateDetails.success || !certificateDetails.data || !certificateDetails.data.ProjectCertificate ) { + throw { + message: CONSTANTS.apiResponses.CERTIFICATE_GENERATION_FAILED + }; + } + + let updateObject = { + "$set" : {} + }; + + // if transaction id is present. + if ( certificateDetails.data.ProjectCertificate.transactionId && + certificateDetails.data.ProjectCertificate.transactionId !== "" + ) { + let transactionIdvalue = certificateDetails.data.ProjectCertificate.transactionId; + const first2 = transactionIdvalue.slice(0, 2); + + if ( first2 === "1-" ) { + transactionIdvalue = transactionIdvalue.split(/1-(.*)/s) + updateObject["$set"]["certificate.transactionId"] = transactionIdvalue[1]; + } else { + updateObject["$set"]["certificate.transactionId"] = transactionIdvalue; + } + + } + + // update project details certificate details + if ( certificateDetails.data.ProjectCertificate.osid && + certificateDetails.data.ProjectCertificate.osid !== "" + ) { + updateObject["$set"]["certificate.osid"] = certificateDetails.data.ProjectCertificate.osid; + updateObject["$set"]["certificate.issuedOn"] = new Date(); + } + updateObject["$set"]["certificate.transactionIdCreatedAt"] = new Date();; + + if ( Object.keys(updateObject["$set"]).length > 0 ) { + let updatedProject = await projectQueries.findOneAndUpdate( + { + _id: projectId + }, + updateObject + ); + } + return resolve( { + success: true + }); + } catch (error) { + return resolve({ + success: false, + message: error.message + + }); + } + }) + } + + /** + * certificate callback + * @method + * @name certificateCallback + * @param {String} transactionId - transactionId for create certificate. + * @param {String} osid - osid for created certificate. + * @returns {JSON} certificate data updation details. + */ + + static certificateCallback(transactionId, osid) { + return new Promise(async (resolve, reject) => { + try { + // adding comments to check call back is called properly or not + console.log("<==================callback called====================>",transactionId,osid) + console.log("transactionId :",transactionId) + console.log("osid :",osid) + console.log("<==================callback called====================>") + // callback request structure nested so validating transactionId and osid here instead in validator. + if ( transactionId == "" || osid == "" ) { + throw { + status: HTTP_STATUS_CODE["bad_request"].status, + message: CONSTANTS.apiResponses.TRANSACTION_ID_AND_OSID_REQUIRED + } + } + let updateObject = { + "$set" : {} + }; + + // update osid and eligibility based on transactionId + updateObject["$set"]["certificate.osid"] = osid; + updateObject["$set"]["certificate.message"] = CONSTANTS.common.PROJECT_CERTIFICATE_GENERATED_SUCCESSFULLY; + updateObject["$set"]["certificate.issuedOn"] = new Date(); + + let projectDetails = await projectQueries.findOneAndUpdate( + { + "certificate.transactionId" : transactionId + }, + updateObject + ); + + if ( projectDetails == null || !Object.keys(projectDetails).length > 0 ) { + throw { + status: HTTP_STATUS_CODE["bad_request"].status, + message: CONSTANTS.apiResponses.PROJECT_NOT_FOUND + } + } + await kafkaProducersHelper.pushProjectToKafka(projectDetails); + return resolve({ + success: true, + message: CONSTANTS.apiResponses.PROJECT_CERTIFICATE_GENERATED, + data : { + _id : ObjectId(projectDetails._id) + } + + }); + + } catch (error) { + return resolve({ + success: false, + message: error.message, + data: {} + }); + } + }) + } + + /** + * List user project details with certificate + * @method + * @name certificates + * @param {String} userId - userId. + * @returns {JSON} certificate data updation details. + */ + + static certificates(userId) { + return new Promise(async (resolve, reject) => { + try { + + // get project details of user which have certificate. + const userProject = await projectQueries.projectDocument({ + userId: userId, + status: CONSTANTS.common.SUBMITTED_STATUS, + certificate: {$exists:true} + }, [ + "_id", + "title", + "status", + "certificate.osid", + "certificate.transactioId", + "certificate.templateUrl", + "certificate.status", + "certificate.eligible", + "certificate.message", + "certificate.issuedOn", + "completedDate" + ]); + + if ( !userProject.length > 0 ) { + throw { + status: HTTP_STATUS_CODE["bad_request"].status, + message: CONSTANTS.apiResponses.PROJECT_WITH_CERTIFICATE_NOT_FOUND + } + } + let templateFilePath = []; + //loop through user projects and get downloadable url for templateUrl if osid is present. + for( let userProjectPointer = 0; userProjectPointer < userProject.length; userProjectPointer++ ) { + if ( userProject[userProjectPointer].certificate.osid && + userProject[userProjectPointer].certificate.osid !== "" && + userProject[userProjectPointer].certificate.templateUrl && + userProject[userProjectPointer].certificate.templateUrl !== "" + ) { + templateFilePath.push(userProject[userProjectPointer].certificate.templateUrl); + } + } + + if( templateFilePath.length > 0 ) { + + let certificateTemplateDownloadableUrl = + await coreService.getDownloadableUrl( + { + filePaths: templateFilePath + } + ); + if ( !certificateTemplateDownloadableUrl.success ) { + throw { + message: CONSTANTS.apiResponses.DOWNLOADABLE_URL_NOT_FOUND + }; + } + // map downloadable templateUrl to corresponding project data + userProject.forEach(projectData => { + var itemFromUrlArray = certificateTemplateDownloadableUrl.data.find(item=> item.filePath == projectData.certificate.templateUrl); + if (itemFromUrlArray) { + projectData.certificate.templateUrl = itemFromUrlArray.url; + } + } + ) + } + + let count = _.countBy(userProject, (rec) => { + return (rec.certificate && rec.certificate.osid && rec.certificate.osid !== "" )? 'generated': 'notGenerated'; + }); + + return resolve({ + success: true, + message: CONSTANTS.apiResponses.PROJECTS_FETCHED, + data : { + data : userProject, + count : userProject.length, + certificateCount : count.generated + } + + }); + } catch (error) { + return resolve({ + success: false, + message: error.message, + data: {} + }); + } + }) + } + + /** + * Re-Issue project certificate + * @method + * @name certificateReIssue + * @param {String} projectId - projectId. + * @returns {JSON} certificate re-issued details. + */ + + static certificateReIssue(projectId) { + return new Promise(async (resolve, reject) => { + try { + // get project details project for which certificate re-issue required . + const userProject = await projectQueries.projectDocument({ + _id: projectId, + status: CONSTANTS.common.SUBMITTED_STATUS, + certificate: {$exists:true} + }); + + // if project details not found. + if (!userProject.length > 0) { + throw { + status: HTTP_STATUS_CODE['bad_request'].status, + message: CONSTANTS.apiResponses.USER_PROJECT_NOT_FOUND + }; + } + let updateObject = { + "$set" : {} + }; + + // fetch user data using userId of project and calling the profile API + let userProfileData = await userProfileService.profileReadPrivate(userProject[0].userId); + if ( userProfileData.success && + userProfileData.data && + userProfileData.data.response && + userProfileData.data.response.firstName && + userProfileData.data.response.firstName !== "" + ) { + userProject[0].userProfile.firstName = userProfileData.data.response.firstName; + userProject[0].userProfile.lastName = userProfileData.data.response.lastName; + } else { + throw { + status: HTTP_STATUS_CODE['bad_request'].status, + message: CONSTANTS.apiResponses.USER_PROFILE_NOT_FOUND + }; + } + + // create payload for certificate generation + const certificateData = await this.createCertificatePayload(userProject[0]); + + // call sunbird-RC to create certificate for project + const certificate = await this.createCertificate(certificateData, userProject[0]._id); + + if ( !certificate.success ) { + throw { + message: CONSTANTS.apiResponses.CERTIFICATE_GENERATION_FAILED + }; + } + + if ( userProject[0].certificate.transactionId ) { + updateObject["$set"]["certificate.originalTransactionInformation.transactionId"] = userProject[0].certificate.transactionId + } + if ( userProject[0].certificate.osid ) { + updateObject["$set"]["certificate.originalTransactionInformation.osid"] = userProject[0].certificate.osid; + } + + if (userProject[0].userProfile.firstName ) { + updateObject["$set"]["userProfile.firstName"] = userProject[0].userProfile.firstName; + } + + if (userProject[0].userProfile.lastName ) { + updateObject["$set"]["userProfile.lastName"] = userProject[0].userProfile.lastName; + } + + updateObject["$set"]["certificate.reIssuedAt"] = new Date(); + await projectQueries.findOneAndUpdate( + { + _id: userProject[0]._id + }, + updateObject + ); + + return resolve({ + success: true, + message: CONSTANTS.apiResponses.PROJECT_SUBMITTED_FOR_REISSUE, + data : { + _id : userProject[0]._id + } + + }); + } catch (error) { + return resolve({ + success: false, + message: error.message, + data: {} + }); + } + }) + } + + }; /** @@ -2314,7 +2917,7 @@ function _projectInformation(project) { return new Promise(async (resolve, reject) => { try { - + if (project.entityInformation) { project.entityId = project.entityInformation._id; project.entityName = project.entityInformation.name; @@ -2324,7 +2927,7 @@ function _projectInformation(project) { project.programId = project.programInformation._id; project.programName = project.programInformation.name; } - + //project attachments if ( project.attachments && project.attachments.length > 0 ) { @@ -2351,7 +2954,7 @@ function _projectInformation(project) { } } - + //task attachments if (project.tasks && project.tasks.length > 0) { //order task based on task sequence @@ -2398,7 +3001,7 @@ function _projectInformation(project) { project.tasks = taskAttachmentsUrl.data; } } - + project.status = project.status ? project.status : CONSTANTS.common.NOT_STARTED_STATUS; @@ -2414,7 +3017,6 @@ function _projectInformation(project) { delete project.solutionInformation; delete project.programInformation; - return resolve({ success: true, data: project @@ -2555,7 +3157,7 @@ function _attachmentInformation ( attachmentWithSourcePath = [], linkAttachments function _projectTask(tasks, isImportedFromLibrary = false, parentTaskId = "") { tasks.forEach(singleTask => { - + singleTask.externalId = singleTask.externalId ? singleTask.externalId : singleTask.name.toLowerCase(); singleTask.type = singleTask.type ? singleTask.type : CONSTANTS.common.SIMPLE_TASK_TYPE; singleTask.status = singleTask.status ? singleTask.status : CONSTANTS.common.NOT_STARTED_STATUS; @@ -2564,7 +3166,9 @@ function _projectTask(tasks, isImportedFromLibrary = false, parentTaskId = "") { if (!singleTask.hasOwnProperty("isDeletable")) { singleTask.isDeletable = true; } - + if ( UTILS.isValidMongoId(singleTask._id.toString()) ) { + singleTask.referenceId = singleTask._id.toString(); + } singleTask.createdAt = singleTask.createdAt ? singleTask.createdAt : new Date(); singleTask.updatedAt = new Date(); singleTask._id = UTILS.isValidMongoId(singleTask._id.toString()) ? uuidv4() : singleTask._id; @@ -2603,7 +3207,7 @@ function _projectTask(tasks, isImportedFromLibrary = false, parentTaskId = "") { } }) - + return tasks; } diff --git a/module/userProjects/validator/v1.js b/module/userProjects/validator/v1.js index 81c87ce9..09dcbfa6 100644 --- a/module/userProjects/validator/v1.js +++ b/module/userProjects/validator/v1.js @@ -25,6 +25,14 @@ module.exports = (req) => { }, share : function () { req.checkParams('_id').exists().withMessage("required project id"); + }, + certificateReIssue : function () { + req.checkParams('_id').exists().withMessage("required project id"); + }, + certificateCallback : function () { + req.checkBody("data").exists().withMessage("data is required"); + req.checkBody("data.transactionId").exists().withMessage("transactionId is required"); + req.checkBody("data.osid").exists().withMessage("osid is required"); } } diff --git a/routes/index.js b/routes/index.js index cad0fe2a..d9ada824 100644 --- a/routes/index.js +++ b/routes/index.js @@ -89,7 +89,7 @@ module.exports = function (app) { } console.log('-------------------Response log starts here-------------------'); - console.log(result); + console.log(JSON.stringify(result)); console.log('-------------------Response log ends here-------------------'); } catch (error) {