From e9cf645d1a8623c47364556555114b614d89a09b Mon Sep 17 00:00:00 2001 From: vaishali k Date: Thu, 8 Jan 2026 05:50:17 +0530 Subject: [PATCH 01/44] Issue #251045 feat: Hierarchical Categories Implementation --- .../elevate-project/configs.json | 14 + controllers/v1/library/categories.js | 35 +- controllers/v1/project/templates.js | 16 +- generics/constants/api-responses.js | 2 + models/project-categories.js | 32 ++ module/library/categories/helper.js | 491 +++++++++++++++++- module/library/categories/validator/v1.js | 18 + module/project/templates/helper.js | 45 +- 8 files changed, 633 insertions(+), 20 deletions(-) diff --git a/constants/interface-routes/elevate-project/configs.json b/constants/interface-routes/elevate-project/configs.json index 18b611e3..ade91573 100644 --- a/constants/interface-routes/elevate-project/configs.json +++ b/constants/interface-routes/elevate-project/configs.json @@ -905,6 +905,20 @@ ], "service": "project" }, + { + "sourceRoute": "/project/v1/library/categories/delete/:id", + "type": "DELETE", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "project", + "packageName": "elevate-project" + } + ], + "service": "project" + }, { "sourceRoute": "/project/v1/programs/create", "type": "POST", diff --git a/controllers/v1/library/categories.js b/controllers/v1/library/categories.js index f5e9d493..4f79f923 100644 --- a/controllers/v1/library/categories.js +++ b/controllers/v1/library/categories.js @@ -36,7 +36,7 @@ module.exports = class LibraryCategories extends Abstract { } /** - * @api {get} /improvement-project/api/v1/library/categories/projects/:categoryExternalId?page=:page&limit=:limit&search=:search&sort=:sort + * @api {get} /improvement-project/api/v1/library/categories/projects/:categoryExternalId?page=:page&limit=:limit&search=:search&sort=:sort * List of library projects. * @apiVersion 1.0.0 * @apiGroup Library Categories @@ -56,7 +56,7 @@ module.exports = class LibraryCategories extends Abstract { "description" : "Test template description", "createdAt": "2020-08-31T05:59:12.230Z" } - ], + ], "count": 7 } } @@ -179,7 +179,7 @@ module.exports = class LibraryCategories extends Abstract { } /** - * @api {get} /improvement-project/api/v1/library/categories/list + * @api {get} /improvement-project/api/v1/library/categories/list * List of library categories. * @apiVersion 1.0.0 * @apiGroup Library Categories @@ -254,4 +254,33 @@ module.exports = class LibraryCategories extends Abstract { } }) } + + /** + * List of library categories + * @method + * @name list + * @param {Object} req - requested data + * @returns {Array} Library categories. + */ + + async delete(req) { + return new Promise(async (resolve, reject) => { + try { + const filterQuery = { + _id: req.params._id, + } + let projectCategories = await libraryCategoriesHelper.delete(filterQuery, req.userDetails) + + projectCategories.result = projectCategories.data + + return resolve(projectCategories) + } 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, + }) + } + }) + } } diff --git a/controllers/v1/project/templates.js b/controllers/v1/project/templates.js index 9456e14f..8250ead4 100644 --- a/controllers/v1/project/templates.js +++ b/controllers/v1/project/templates.js @@ -125,12 +125,12 @@ module.exports = class ProjectTemplates extends Abstract { } /** - * @api {post} /project/v1/project/templates/importProjectTemplate/:projectTemplateExternalId + * @api {post} /project/v1/project/templates/importProjectTemplate/:projectTemplateExternalId * Import templates from existsing project templates. * @apiVersion 1.0.0 * @apiGroup Project Templates * @apiSampleRequest /project/v1/project/templates/importProjectTemplate/template-1 - * @apiParamExample {json} Request: + * @apiParamExample {json} Request: * { * "externalId" : "template1", "isReusable" : false, @@ -187,7 +187,7 @@ module.exports = class ProjectTemplates extends Abstract { * @apiVersion 1.0.0 * @apiGroup Project Templates * @apiSampleRequest /project/v1/project/templates/listByIds - * @apiParamExample {json} Request: + * @apiParamExample {json} Request: * { * "externalIds" : ["IDEAIMP 4"] * } @@ -680,13 +680,13 @@ module.exports = class ProjectTemplates extends Abstract { } /** - * @api {post} /project/v1/project/templates/update/:templateId + * @api {post} /project/v1/project/templates/update/:templateId * Update projects template. * @apiVersion 1.0.0 * @apiGroup Project Templates * @apiSampleRequest /project/v1/project/templates/update/6006b5cca1a95727dbcdf648 - * @apiHeader {String} internal-access-token internal access token - * @apiHeader {String} X-authenticated-user-token Authenticity token + * @apiHeader {String} internal-access-token internal access token + * @apiHeader {String} X-authenticated-user-token Authenticity token * @apiUse successBody * @apiUse errorBody * @apiParamExample {json} Response: @@ -822,7 +822,9 @@ module.exports = class ProjectTemplates extends Abstract { req.pageSize, req.searchText, req.query.currentOrgOnly ? req.query.currentOrgOnly : false, - req.userDetails + req.userDetails, + req.query.categoryIds ? req.query.categoryIds : '', + req.query.groupByCategory ? req.query.groupByCategory : false ) // Assign the 'data' property of 'projectTemplates' to 'result'. diff --git a/generics/constants/api-responses.js b/generics/constants/api-responses.js index 216e3eb3..1e3c1565 100644 --- a/generics/constants/api-responses.js +++ b/generics/constants/api-responses.js @@ -80,6 +80,8 @@ module.exports = { PROJECT_CATEGORIES_ADDED: 'Successfully created project categories', PROJECT_CATEGORIES_NOT_UPDATED: 'Could not updated project categories', PROJECT_CATEGORIES_NOT_ADDED: 'Could not create project categories', + PROJECT_CATEGORIES_DELETED: 'Successfully deleted project categories', + PROJECT_CATEGORIES_NOT_DELETED: 'Could not delete project categories', PROJECT_TEMPLATE_NOT_UPDATED: 'Not found project template', COULD_NOT_CREATE_ASSESSMENT_SOLUTION: 'Could not create assessment solution', FAILED_TO_ADD_ENTITY_TO_SOLUTION: 'Failed to add entity to solution', diff --git a/models/project-categories.js b/models/project-categories.js index 290a2917..ead915e5 100644 --- a/models/project-categories.js +++ b/models/project-categories.js @@ -66,11 +66,43 @@ module.exports = { default: [], index: true, }, + description: { + type: String, + index: true, + required: true, + default: 'default', + }, + keywords: { + type: Array, + default: [], + index: true, + }, + parentId: { + type: 'ObjectId', + default: null, + index: true, // CRITICAL for hierarchy queries + }, + hasChildCategories: { + type: Boolean, + default: false, + index: true, // Quick leaf identification + }, + sequenceNumber: { + type: Number, + default: 0, + index: true, + }, }, compoundIndex: [ { name: { externalId: 1, tenantId: 1 }, indexType: { unique: true }, }, + { + name: { parent_id: 1, tenantId: 1, sequenceNumber: 1 }, + }, + { + name: { tenantId: 1, hasChildCategories: 1 }, + }, ], } diff --git a/module/library/categories/helper.js b/module/library/categories/helper.js index 2bfab450..0ac45a92 100644 --- a/module/library/categories/helper.js +++ b/module/library/categories/helper.js @@ -25,6 +25,144 @@ const orgExtensionQueries = require(DB_QUERY_BASE_PATH + '/organizationExtension */ module.exports = class LibraryCategoriesHelper { + /** + * Validate parentId for category operations + * @method + * @name validateParentId + * @param {String} parentId - parent category id + * @param {String} categoryId - current category id (for update operations) + * @param {String} tenantId - tenant id + * @returns {Object} validation result + */ + static async validateParentId(parentId, categoryId = null, tenantId) { + if (!parentId) { + return { success: true, parentCategory: null } + } + + // Convert to ObjectId safely + const parentObjectId = UTILS.convertStringToObjectId(parentId) + if (!parentObjectId) { + throw { + success: false, + status: HTTP_STATUS_CODE.bad_request.status, + message: 'Invalid parentId format', + } + } + + // Check if parent category exists, is not deleted, and is in the same tenant + const parentCategory = await projectCategoriesQueries.categoryDocuments( + { + _id: parentObjectId, + tenantId: tenantId, + isDeleted: false, + }, + ['_id', 'hasChildCategories', 'parentId'] + ) + + if (!parentCategory || parentCategory.length === 0) { + throw { + success: false, + status: HTTP_STATUS_CODE.bad_request.status, + message: 'Parent category not found or does not belong to the same tenant', + } + } + + const parent = parentCategory[0] + + if (categoryId) { + // Cannot set category as its own parent + if (parentId.toString() === categoryId.toString()) { + throw { + success: false, + status: HTTP_STATUS_CODE.bad_request.status, + message: 'Category cannot be its own parent', + } + } + + // Cannot move category to its own descendant (circular reference) + const isDescendant = await this.isDescendant(parentId, categoryId, tenantId) + if (isDescendant) { + throw { + success: false, + status: HTTP_STATUS_CODE.bad_request.status, + message: 'Cannot move category to its own descendant - would create circular reference', + } + } + } + + return { success: true, parentCategory: parent } + } + + /** + * Check if a category is a descendant of another category + * @method + * @name isDescendant + * @param {String} potentialDescendantId - category to check if it's a descendant + * @param {String} ancestorId - potential ancestor category + * @param {String} tenantId - tenant id + * @returns {Boolean} true if potentialDescendantId is a descendant of ancestorId + */ + static async isDescendant(potentialDescendantId, ancestorId, tenantId) { + let currentId = potentialDescendantId + + while (currentId) { + if (currentId.toString() === ancestorId.toString()) { + return true + } + + const category = await projectCategoriesQueries.categoryDocuments( + { + _id: UTILS.convertStringToObjectId(currentId), + tenantId: tenantId, + isDeleted: false, + }, + ['parentId'] + ) + + if (!category || category.length === 0 || !category[0].parentId) { + break + } + + currentId = category[0].parentId + } + + return false + } + + /** + * Get the hierarchy depth for a given category + * @method + * @name getHierarchyDepth + * @param {String} categoryId - category id + * @param {String} tenantId - tenant id + * @returns {Number} hierarchy depth + */ + static async getHierarchyDepth(categoryId, tenantId) { + let depth = 0 + let currentId = categoryId + + while (currentId && depth < 10) { + // Safety limit to prevent infinite loops + const category = await projectCategoriesQueries.categoryDocuments( + { + _id: UTILS.convertStringToObjectId(currentId), + tenantId: tenantId, + isDeleted: false, + }, + ['parentId'] + ) + + if (!category || category.length === 0 || !category[0].parentId) { + break + } + + currentId = category[0].parentId + depth++ + } + + return depth + } + /** * List of library projects. * @method @@ -80,7 +218,7 @@ module.exports = class LibraryCategoriesHelper { matchQuery['$match']['tenantId'] = userDetails.userInformation.tenantId /** - * + * Sample for matchQuery obj when orgExtension.externalProjectResourceVisibilityPolicy = CURRENT { "$match": { @@ -92,7 +230,7 @@ module.exports = class LibraryCategoriesHelper { } */ /** - * + * Sample for matchQuery obj when orgExtension.externalProjectResourceVisibilityPolicy = ASSOCIATED { "$match": { @@ -122,7 +260,7 @@ module.exports = class LibraryCategoriesHelper { } */ /** - * + * Sample for matchQuery obj when orgExtension.externalProjectResourceVisibilityPolicy = ALL { "$match": { @@ -521,6 +659,7 @@ module.exports = class LibraryCategoriesHelper { matchQuery['tenantId'] = userDetails.tenantAndOrgInfo.tenantId let categoryData = await projectCategoriesQueries.categoryDocuments(matchQuery, 'all') + // Throw error if category is not found if ( !categoryData || @@ -534,6 +673,18 @@ module.exports = class LibraryCategoriesHelper { } } + // Validate parent_id if provided in updateData + if (updateData.parentId !== undefined) { + let parentCategory + const validationResult = await this.validateParentId( + updateData.parentId, + categoryData[0]._id.toString(), + userDetails.tenantAndOrgInfo.tenantId + ) + parentCategory = validationResult.parentCategory + } + + // Handle evidence uploads let evidenceUploadData = await handleEvidenceUpload(files, userDetails.userInformation.userId) evidenceUploadData = evidenceUploadData.data @@ -566,6 +717,38 @@ module.exports = class LibraryCategoriesHelper { } } + // Update hasChildCategories for old and new parents + if (updateData.parentId !== undefined) { + const tenantId = userDetails.tenantAndOrgInfo.tenantId + const categoryId = categoryData[0]._id.toString() + + // Handle old parent: check if it has any other children + if (categoryData[0].parentId && categoryData[0].parentId.toString() !== updateData.parentId) { + const otherChildren = await projectCategoriesQueries.categoryDocuments( + { + parentId: categoryData[0].parentId, + tenantId: tenantId, + isDeleted: false, + status: CONSTANTS.common.ACTIVE, + }, + ['_id'] + ) + + await projectCategoriesQueries.updateMany( + { _id: categoryData[0].parentId }, + { hasChildCategories: otherChildren && otherChildren.length > 0 } + ) + } + + // Handle new parent: set hasChildCategories to true + if (updateData.parentId) { + await projectCategoriesQueries.updateMany( + { _id: updateData.parentId }, + { hasChildCategories: true } + ) + } + } + return resolve({ success: true, message: CONSTANTS.apiResponses.PROJECT_CATEGORIES_UPDATED, @@ -724,6 +907,48 @@ module.exports = class LibraryCategoriesHelper { } } + // Validate parent_id if provided + let parentCategory = null + if (categoryData.parentId) { + const validationResult = await this.validateParentId(categoryData.parentId, null, tenantId) + parentCategory = validationResult.parentCategory + } + + // Auto-assign sequenceNumber based on siblings under same parent + let sequenceNumber = 0 + if (categoryData.parent_id) { + // Find max sequenceNumber among siblings + const siblings = await projectCategoriesQueries.categoryDocuments( + { + parent_id: categoryData.parent_id, + tenantId: tenantId, + isDeleted: false, + }, + ['sequenceNumber'] + ) + + if (siblings && siblings.length > 0) { + const maxSequence = Math.max(...siblings.map((sibling) => sibling.sequenceNumber || 0)) + sequenceNumber = maxSequence + 1 + } + } else { + // For root level categories, find max sequenceNumber among root categories + const rootCategories = await projectCategoriesQueries.categoryDocuments( + { + parent_id: null, + tenantId: tenantId, + isDeleted: false, + }, + ['sequenceNumber'] + ) + + if (rootCategories && rootCategories.length > 0) { + const maxSequence = Math.max(...rootCategories.map((category) => category.sequenceNumber || 0)) + sequenceNumber = maxSequence + 1 + } + } + categoryData.sequenceNumber = sequenceNumber + // Fetch the signed urls from handleEvidenceUpload function const evidences = await handleEvidenceUpload(files, userDetails.userInformation.userId) categoryData['evidences'] = evidences.data @@ -742,6 +967,14 @@ module.exports = class LibraryCategoriesHelper { } } + // Update parent's hasChildCategories to true if parent exists + if (parentCategory && !parentCategory.hasChildCategories) { + await projectCategoriesQueries.updateMany( + { _id: parentCategory._id, tenantId: tenantId }, + { hasChildCategories: true } + ) + } + return resolve({ success: true, message: CONSTANTS.apiResponses.PROJECT_CATEGORIES_ADDED, @@ -777,6 +1010,8 @@ module.exports = class LibraryCategoriesHelper { // create query to fetch assets query['tenantId'] = tenantId + query['status'] = CONSTANTS.common.ACTIVE_STATUS + query['isDeleted'] = false // handle currentOrgOnly filter if (req.query['currentOrgOnly']) { @@ -785,13 +1020,73 @@ module.exports = class LibraryCategoriesHelper { query['orgId'] = { $in: ['ALL', req.userDetails.userInformation.organizationId] } } } - query['status'] = CONSTANTS.common.ACTIVE_STATUS + // Handle parentId query param. Accepts: actual id, omitted, or the string 'null' (for root) + let parentCategory = null + if (req.query.parentId) { + const rawParent = req.query.parentId + // if client sends ?parentId=null or empty string, treat as root (parentId === null) + if (rawParent === 'null' || rawParent === null || rawParent === '') { + query['parentId'] = null + } else { + // Convert to ObjectId safely to avoid mongoose casting errors + const parentObjectId = UTILS.convertStringToObjectId(rawParent) + if (!parentObjectId) { + throw { + success: false, + status: HTTP_STATUS_CODE.bad_request.status, + message: 'Invalid parentId provided', + } + } + // Check if parent category exists, is not deleted, and is in the same tenant + parentCategory = await projectCategoriesQueries.categoryDocuments( + { _id: parentObjectId, tenantId: tenantId, isDeleted: false }, + ['_id', 'hasChildCategories'] + ) + + if (!parentCategory || parentCategory.length === 0) { + throw { + success: false, + status: HTTP_STATUS_CODE.bad_request.status, + message: 'Parent category not found or does not belong to the same tenant', + } + } + query['parentId'] = parentObjectId + } + } + + // Add keywords filter - categories must have at least one of the specified keywords + if (req.query.keywords && req.query.keywords.trim() !== '') { + const keywordsArray = req.query.keywords + .split(',') + .map((k) => k.trim()) + .filter((k) => k !== '') + if (keywordsArray.length > 0) { + query['keywords'] = { $in: keywordsArray } + } + } + + // Add search functionality for name and description (separate from keywords filter) + if (req.searchText && req.searchText.trim() !== '') { + const searchTerm = req.searchText.trim() + query['$or'] = [ + { name: new RegExp(searchTerm, 'i') }, + { description: new RegExp(searchTerm, 'i') }, + { externalId: new RegExp(searchTerm, 'i') }, + ] + } + let categoryData = await projectCategoriesQueries.categoryDocuments(query, [ 'externalId', 'name', 'icon', 'updatedAt', 'noOfProjects', + 'description', + 'keywords', + 'parentId', + 'hasChildCategories', + 'sequenceNumber', + 'metaInformation', ]) if (!categoryData.length > 0) { @@ -815,6 +1110,188 @@ module.exports = class LibraryCategoriesHelper { } }) } + + /** + * Delete library category. + * @method + * @name delete + * @param {Object} filterQuery - filter query + * @param {Object} userDetails - user details + * @returns {Object} Delete operation result + */ + static delete(filterQuery, userDetails) { + return new Promise(async (resolve, reject) => { + try { + const tenantId = userDetails.userInformation.tenantId + const categoryId = filterQuery._id + + // Find the category to delete + let categoryData = await projectCategoriesQueries.categoryDocuments( + { + _id: categoryId, + tenantId: tenantId, + isDeleted: false, + }, + 'all' + ) + + if (!categoryData || categoryData.length === 0) { + throw { + status: HTTP_STATUS_CODE.not_found.status, + message: CONSTANTS.apiResponses.CATEGORY_NOT_FOUND, + } + } + + categoryData = categoryData[0] + + // Check if category has children + const children = await projectCategoriesQueries.categoryDocuments( + { + parentId: categoryId, + tenantId: tenantId, + isDeleted: false, + }, + ['_id'] + ) + + if (children && children.length > 0) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: + 'Cannot delete category that has child categories. Please delete or move child categories first.', + } + } + + // Check if category has associated projects + const associatedTemplates = categoryData.noOfProjects || 0 + + if (associatedTemplates && associatedTemplates.length > 0) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: + 'Cannot delete category that has associated project templates. Please remove the category from templates first.', + } + } + + // Soft delete the category + const updateResult = await projectCategoriesQueries.updateMany( + { _id: categoryId, tenantId: tenantId }, + { + isDeleted: true, + updatedBy: userDetails.userInformation.userId, + updatedAt: new Date(), + } + ) + + if (!updateResult) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.PROJECT_CATEGORIES_NOT_DELETED, + } + } + + // Update parent's hasChildCategories if this was the last child + if (categoryData.parentId) { + const siblings = await projectCategoriesQueries.categoryDocuments( + { + parentId: categoryData.parentId, + tenantId: tenantId, + isDeleted: false, + _id: { $ne: categoryId }, // Exclude the deleted category + }, + ['_id'] + ) + + await projectCategoriesQueries.updateMany( + { _id: categoryData.parentId }, + { hasChildCategories: siblings && siblings.length > 0 } + ) + } + + return resolve({ + success: true, + message: CONSTANTS.apiResponses.PROJECT_CATEGORIES_DELETED, + }) + } catch (error) { + return resolve({ + success: false, + message: error.message, + data: {}, + }) + } + }) + } + + /** + * Get library category. + * @method + * @name details + * @param {Object} filterQuery - filter query + * @param {Object} userDetails - user details + * @returns {Object} Category result + */ + static details(filterQuery, userDetails) { + return new Promise(async (resolve, reject) => { + try { + let tenantId = userDetails.userInformation.tenantId + let organizationId = userDetails.userInformation.organizationId + + let matchQuery = { + _id: filterQuery._id, + tenantId: tenantId, + status: CONSTANTS.common.ACTIVE_STATUS, + isDeleted: false, + } + + let categoryData = await projectCategoriesQueries.categoryDocuments(matchQuery, 'all') + + if (!categoryData || categoryData.length === 0) { + throw { + status: HTTP_STATUS_CODE.not_found.status, + message: CONSTANTS.apiResponses.CATEGORY_NOT_FOUND, + } + } + + // If getChildren is true, fetch immediate children + if (filterQuery.getChildren) { + let childrenQuery = { + parentId: filterQuery._id, + tenantId: tenantId, + status: CONSTANTS.common.ACTIVE_STATUS, + isDeleted: false, + } + + let children = await projectCategoriesQueries.categoryDocuments(childrenQuery, [ + 'externalId', + 'name', + 'icon', + 'updatedAt', + 'noOfProjects', + 'description', + 'keywords', + 'parentId', + 'hasChildCategories', + 'sequenceNumber', + 'metaInformation', + ]) + + categoryData[0].children = children + } + + return resolve({ + success: true, + message: CONSTANTS.apiResponses.PROJECT_CATEGORIES_FETCHED, + data: categoryData[0], + }) + } catch (error) { + return resolve({ + success: false, + message: error.message, + data: {}, + }) + } + }) + } } /** @@ -916,7 +1393,7 @@ function handleEvidenceUpload(files, userId) { * @returns {Object} returns modified matchQuery */ /** - * + * Sample for matchQuery obj when orgExtension.externalProjectResourceVisibilityPolicy = CURRENT { "$match": { @@ -928,7 +1405,7 @@ function handleEvidenceUpload(files, userId) { } */ /** - * + * Sample for matchQuery obj when orgExtension.externalProjectResourceVisibilityPolicy = ASSOCIATED { "$match": { @@ -958,7 +1435,7 @@ function handleEvidenceUpload(files, userId) { } */ /** - * + * Sample for matchQuery obj when orgExtension.externalProjectResourceVisibilityPolicy = ALL { "$match": { diff --git a/module/library/categories/validator/v1.js b/module/library/categories/validator/v1.js index ec843e6c..7ea7daa4 100644 --- a/module/library/categories/validator/v1.js +++ b/module/library/categories/validator/v1.js @@ -10,6 +10,24 @@ module.exports = (req) => { create: function () { req.checkBody('externalId').exists().withMessage('externalId is required') req.checkBody('name').exists().withMessage('name is required') + req.checkBody('description').optional().isString().withMessage('description must be a string') + req.checkBody('keywords').optional().isArray().withMessage('keywords must be an array') + if (req.body.keywords) { + req.body.keywords.forEach((keyword, index) => { + req.checkBody(`keywords[${index}]`) + .isString() + .withMessage(`keyword at index ${index} must be a string`) + }) + } + req.checkBody('parent_id').optional().isMongoId().withMessage('parent_id must be a valid ObjectId') + req.checkBody('hasChildCategories') + .not() + .exists() + .withMessage('hasChildCategories cannot be set in request body') + req.checkBody('sequenceNumber') + .optional() + .isInt({ min: 0 }) + .withMessage('sequenceNumber must be a non-negative integer') }, update: function () { req.checkParams('_id').exists().withMessage('required category id') diff --git a/module/project/templates/helper.js b/module/project/templates/helper.js index 67f7b1af..99f5d9e2 100644 --- a/module/project/templates/helper.js +++ b/module/project/templates/helper.js @@ -1982,7 +1982,15 @@ module.exports = class ProjectTemplatesHelper { * @returns {Object} - project templates list. */ - static list(pageNo = '', pageSize = '', searchText = '', currentOrgOnly = false, userDetails) { + static list( + pageNo = '', + pageSize = '', + searchText = '', + currentOrgOnly = false, + userDetails, + categoryIds = '', + groupByCategory = false + ) { return new Promise(async (resolve, reject) => { try { // Create a query object with the 'isReusable' property set to true. @@ -2006,6 +2014,22 @@ module.exports = class ProjectTemplatesHelper { ] } + // If 'categoryIds' are provided, add a filter for categories. + if (categoryIds && categoryIds !== '') { + const categoryIdArray = categoryIds + .split(',') + .map((id) => id.trim()) + .filter((id) => id !== '') + if (categoryIdArray.length > 0) { + // Convert category IDs to ObjectIds if needed + const categoryObjectIds = categoryIdArray.map((id) => { + return UTILS.convertStringToObjectId(id) || id + }) + // Filter by categories._id to match category objects within the categories array + queryObject['categories._id'] = { $in: categoryObjectIds } + } + } + // Call the 'templateDocument' function from 'projectTemplateQueries' // using the 'queryObject' to fetch templates. const templates = await projectTemplateQueries.templateDocument(queryObject) @@ -2015,8 +2039,23 @@ module.exports = class ProjectTemplatesHelper { const endIndex = pageNo * pageSize // Slice the 'templates' array to get paginated results. - const paginatedResults = templates.slice(startIndex, endIndex) - + let paginatedResults = templates.slice(startIndex, endIndex) + + if (groupByCategory) { + let groupedTemplates = {} + paginatedResults.forEach((template) => { + if (template.categories && template.categories.length > 0) { + template.categories.forEach((category) => { + const catId = category._id.toString() // Convert ObjectId to string for key + if (!groupedTemplates[catId]) { + groupedTemplates[catId] = [] + } + groupedTemplates[catId].push(template) + }) + } + }) + paginatedResults = groupedTemplates + } // Resolve the promise with success, message, and paginated data. return resolve({ success: true, From 554d27ab0fd579257cb3ae6ed31e225a7257f4aa Mon Sep 17 00:00:00 2001 From: vaishali k Date: Thu, 8 Jan 2026 05:54:37 +0530 Subject: [PATCH 02/44] added details API --- .../elevate-project/configs.json | 14 ++++++++ controllers/v1/library/categories.js | 32 ++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/constants/interface-routes/elevate-project/configs.json b/constants/interface-routes/elevate-project/configs.json index ade91573..b2f9eb05 100644 --- a/constants/interface-routes/elevate-project/configs.json +++ b/constants/interface-routes/elevate-project/configs.json @@ -919,6 +919,20 @@ ], "service": "project" }, + { + "sourceRoute": "/project/v1/library/categories/details/:id", + "type": "GET", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "project", + "packageName": "elevate-project" + } + ], + "service": "project" + }, { "sourceRoute": "/project/v1/programs/create", "type": "POST", diff --git a/controllers/v1/library/categories.js b/controllers/v1/library/categories.js index 4f79f923..a427c26f 100644 --- a/controllers/v1/library/categories.js +++ b/controllers/v1/library/categories.js @@ -256,7 +256,7 @@ module.exports = class LibraryCategories extends Abstract { } /** - * List of library categories + * delete a library category * @method * @name list * @param {Object} req - requested data @@ -283,4 +283,34 @@ module.exports = class LibraryCategories extends Abstract { } }) } + + /** + * read a library category + * @method + * @name list + * @param {Object} req - requested data + * @returns {Array} Library categories. + */ + + async details(req) { + return new Promise(async (resolve, reject) => { + try { + const filterQuery = { + _id: req.params._id, + getChildren: req.query.getChildren === 'true', + } + let projectCategories = await libraryCategoriesHelper.details(filterQuery, req.userDetails) + + projectCategories.result = projectCategories.data + + return resolve(projectCategories) + } 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, + }) + } + }) + } } From 030f146a15fd4b9e4503aab0f935b2358c9771c8 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Thu, 8 Jan 2026 17:33:34 +0530 Subject: [PATCH 03/44] Issue #000 fix: metainformation added --- controllers/v1/library/categories.js | 4 +- controllers/v1/project/templates.js | 3 +- models/project-categories.js | 4 ++ module/library/categories/helper.js | 58 ++++++++++++++++------------ module/project/templates/helper.js | 17 +++++++- 5 files changed, 58 insertions(+), 28 deletions(-) diff --git a/controllers/v1/library/categories.js b/controllers/v1/library/categories.js index a427c26f..5dec79c4 100644 --- a/controllers/v1/library/categories.js +++ b/controllers/v1/library/categories.js @@ -240,7 +240,7 @@ module.exports = class LibraryCategories extends Abstract { async list(req) { return new Promise(async (resolve, reject) => { try { - let projectCategories = await libraryCategoriesHelper.list(req) + let projectCategories = await libraryCategoriesHelper.list(req.searchText, req.query, req.userDetails) projectCategories.result = projectCategories.data @@ -287,7 +287,7 @@ module.exports = class LibraryCategories extends Abstract { /** * read a library category * @method - * @name list + * @name details * @param {Object} req - requested data * @returns {Array} Library categories. */ diff --git a/controllers/v1/project/templates.js b/controllers/v1/project/templates.js index 8250ead4..6994cf7f 100644 --- a/controllers/v1/project/templates.js +++ b/controllers/v1/project/templates.js @@ -824,7 +824,8 @@ module.exports = class ProjectTemplates extends Abstract { req.query.currentOrgOnly ? req.query.currentOrgOnly : false, req.userDetails, req.query.categoryIds ? req.query.categoryIds : '', - req.query.groupByCategory ? req.query.groupByCategory : false + req.query.groupByCategory ? req.query.groupByCategory : false, + req.query.taskDetails ? req.query.taskDetails : false ) // Assign the 'data' property of 'projectTemplates' to 'result'. diff --git a/models/project-categories.js b/models/project-categories.js index ead915e5..5516021b 100644 --- a/models/project-categories.js +++ b/models/project-categories.js @@ -92,6 +92,10 @@ module.exports = { default: 0, index: true, }, + metaInformation: { + type: Object, + default: {}, + }, }, compoundIndex: [ { diff --git a/module/library/categories/helper.js b/module/library/categories/helper.js index 0ac45a92..d1094c82 100644 --- a/module/library/categories/helper.js +++ b/module/library/categories/helper.js @@ -999,11 +999,11 @@ module.exports = class LibraryCategoriesHelper { * @returns {Object} category details */ - static list(req) { + static list(searchText, params, userDetails) { return new Promise(async (resolve, reject) => { try { - let tenantId = req.userDetails.userInformation.tenantId - let organizationId = req.userDetails.userInformation.organizationId + let tenantId = userDetails.userInformation.tenantId + let organizationId = userDetails.userInformation.organizationId let query = { visibleToOrganizations: { $in: [organizationId] }, } @@ -1014,16 +1014,16 @@ module.exports = class LibraryCategoriesHelper { query['isDeleted'] = false // handle currentOrgOnly filter - if (req.query['currentOrgOnly']) { - let currentOrgOnly = UTILS.convertStringToBoolean(req.query['currentOrgOnly']) + if (params['currentOrgOnly']) { + let currentOrgOnly = UTILS.convertStringToBoolean(params['currentOrgOnly']) if (currentOrgOnly) { query['orgId'] = { $in: ['ALL', req.userDetails.userInformation.organizationId] } } } // Handle parentId query param. Accepts: actual id, omitted, or the string 'null' (for root) let parentCategory = null - if (req.query.parentId) { - const rawParent = req.query.parentId + if (params.parentId) { + const rawParent = params.parentId // if client sends ?parentId=null or empty string, treat as root (parentId === null) if (rawParent === 'null' || rawParent === null || rawParent === '') { query['parentId'] = null @@ -1055,8 +1055,8 @@ module.exports = class LibraryCategoriesHelper { } // Add keywords filter - categories must have at least one of the specified keywords - if (req.query.keywords && req.query.keywords.trim() !== '') { - const keywordsArray = req.query.keywords + if (params.keywords && params.keywords.trim() !== '') { + const keywordsArray = params.keywords .split(',') .map((k) => k.trim()) .filter((k) => k !== '') @@ -1066,8 +1066,8 @@ module.exports = class LibraryCategoriesHelper { } // Add search functionality for name and description (separate from keywords filter) - if (req.searchText && req.searchText.trim() !== '') { - const searchTerm = req.searchText.trim() + if (searchText && searchText.trim() !== '') { + const searchTerm = searchText.trim() query['$or'] = [ { name: new RegExp(searchTerm, 'i') }, { description: new RegExp(searchTerm, 'i') }, @@ -1075,19 +1075,8 @@ module.exports = class LibraryCategoriesHelper { ] } - let categoryData = await projectCategoriesQueries.categoryDocuments(query, [ - 'externalId', - 'name', - 'icon', - 'updatedAt', - 'noOfProjects', - 'description', - 'keywords', - 'parentId', - 'hasChildCategories', - 'sequenceNumber', - 'metaInformation', - ]) + const skipFields = ['__v', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'] + let categoryData = await projectCategoriesQueries.categoryDocuments(query, 'all', skipFields) if (!categoryData.length > 0) { throw { @@ -1096,6 +1085,27 @@ module.exports = class LibraryCategoriesHelper { } } + // If getChildren is true, fetch immediate children for each category + if (params.getChildren) { + for (let category of categoryData) { + let childrenQuery = { + parentId: category._id, + tenantId: tenantId, + status: CONSTANTS.common.ACTIVE_STATUS, + isDeleted: false, + } + + let children = await projectCategoriesQueries.categoryDocuments( + childrenQuery, + 'all', + skipFields + ) + + category.children = children + category.childrenCount = children.length + } + } + return resolve({ success: true, message: CONSTANTS.apiResponses.PROJECT_CATEGORIES_FETCHED, diff --git a/module/project/templates/helper.js b/module/project/templates/helper.js index 99f5d9e2..afc82b04 100644 --- a/module/project/templates/helper.js +++ b/module/project/templates/helper.js @@ -1989,7 +1989,8 @@ module.exports = class ProjectTemplatesHelper { currentOrgOnly = false, userDetails, categoryIds = '', - groupByCategory = false + groupByCategory = false, + taskDetails = false ) { return new Promise(async (resolve, reject) => { try { @@ -2041,6 +2042,20 @@ module.exports = class ProjectTemplatesHelper { // Slice the 'templates' array to get paginated results. let paginatedResults = templates.slice(startIndex, endIndex) + if (taskDetails) { + for (const template of paginatedResults) { + // Fetch tasks and subtasks for each template in paginated results + if (template.tasks && template.tasks.length > 0) { + template.tasks = await this.tasksAndSubTasks( + template._id, + '', + userDetails.userInformation.tenantId, + userDetails.userInformation.organizationId + ) + } + } + } + if (groupByCategory) { let groupedTemplates = {} paginatedResults.forEach((template) => { From ceb95983ee20fdf0201d4b55e9a15587333c59a3 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Thu, 8 Jan 2026 23:21:02 +0530 Subject: [PATCH 04/44] Issue #000 fix: parent_id is not defined --- models/project-categories.js | 2 +- module/library/categories/helper.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/models/project-categories.js b/models/project-categories.js index 5516021b..3ab91ccc 100644 --- a/models/project-categories.js +++ b/models/project-categories.js @@ -103,7 +103,7 @@ module.exports = { indexType: { unique: true }, }, { - name: { parent_id: 1, tenantId: 1, sequenceNumber: 1 }, + name: { parentId: 1, tenantId: 1, sequenceNumber: 1 }, }, { name: { tenantId: 1, hasChildCategories: 1 }, diff --git a/module/library/categories/helper.js b/module/library/categories/helper.js index d1094c82..9da1377e 100644 --- a/module/library/categories/helper.js +++ b/module/library/categories/helper.js @@ -916,11 +916,11 @@ module.exports = class LibraryCategoriesHelper { // Auto-assign sequenceNumber based on siblings under same parent let sequenceNumber = 0 - if (categoryData.parent_id) { + if (categoryData.parentId) { // Find max sequenceNumber among siblings const siblings = await projectCategoriesQueries.categoryDocuments( { - parent_id: categoryData.parent_id, + parentId: categoryData.parentId, tenantId: tenantId, isDeleted: false, }, @@ -935,7 +935,7 @@ module.exports = class LibraryCategoriesHelper { // For root level categories, find max sequenceNumber among root categories const rootCategories = await projectCategoriesQueries.categoryDocuments( { - parent_id: null, + parentId: null, tenantId: tenantId, isDeleted: false, }, @@ -1175,7 +1175,7 @@ module.exports = class LibraryCategoriesHelper { // Check if category has associated projects const associatedTemplates = categoryData.noOfProjects || 0 - if (associatedTemplates && associatedTemplates.length > 0) { + if (associatedTemplates > 0) { throw { status: HTTP_STATUS_CODE.bad_request.status, message: From 173caa3fc8a8630dc68768c70b78279b6aaa6159 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Thu, 8 Jan 2026 23:29:29 +0530 Subject: [PATCH 05/44] Issue #000 fix: parent_id is not defined --- module/project/templates/helper.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/module/project/templates/helper.js b/module/project/templates/helper.js index afc82b04..b0200826 100644 --- a/module/project/templates/helper.js +++ b/module/project/templates/helper.js @@ -1997,7 +1997,8 @@ module.exports = class ProjectTemplatesHelper { // Create a query object with the 'isReusable' property set to true. let queryObject = { isReusable: true } currentOrgOnly = UTILS.convertStringToBoolean(currentOrgOnly) - + groupByCategory = UTILS.convertStringToBoolean(groupByCategory) + taskDetails = UTILS.convertStringToBoolean(taskDetails) queryObject['tenantId'] = userDetails.userInformation.tenantId // handle currentOrgOnly filter From 5aa79a817003f87515227632d71468aeb887c8ca Mon Sep 17 00:00:00 2001 From: vaishali k Date: Fri, 9 Jan 2026 00:28:51 +0530 Subject: [PATCH 06/44] Coderabbit comments resolved --- module/project/templates/helper.js | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/module/project/templates/helper.js b/module/project/templates/helper.js index b0200826..259f70fb 100644 --- a/module/project/templates/helper.js +++ b/module/project/templates/helper.js @@ -2017,16 +2017,21 @@ module.exports = class ProjectTemplatesHelper { } // If 'categoryIds' are provided, add a filter for categories. + let categoryIdArray + if (categoryIds && categoryIds !== '') { - const categoryIdArray = categoryIds + categoryIdArray = categoryIds .split(',') .map((id) => id.trim()) .filter((id) => id !== '') if (categoryIdArray.length > 0) { // Convert category IDs to ObjectIds if needed const categoryObjectIds = categoryIdArray.map((id) => { - return UTILS.convertStringToObjectId(id) || id + const objectId = UTILS.convertStringToObjectId(id) || id + //requestedCategorySet.add(id); + return objectId }) + // Filter by categories._id to match category objects within the categories array queryObject['categories._id'] = { $in: categoryObjectIds } } @@ -2058,15 +2063,24 @@ module.exports = class ProjectTemplatesHelper { } if (groupByCategory) { - let groupedTemplates = {} paginatedResults.forEach((template) => { if (template.categories && template.categories.length > 0) { - template.categories.forEach((category) => { - const catId = category._id.toString() // Convert ObjectId to string for key - if (!groupedTemplates[catId]) { - groupedTemplates[catId] = [] + template.categories.forEach((categoryId) => { + const catId = categoryId.toString() // Convert ObjectId to string for key + if (categoryIdArray && categoryIdArray.length > 0) { + // Check if the current categoryId is in the requestedCategorySet + if (categoryIdArray.includes(catId)) { + if (!groupedTemplates[catId]) { + groupedTemplates[catId] = [] + } + groupedTemplates[catId].push(template) + } + } else { + if (!groupedTemplates[catId]) { + groupedTemplates[catId] = [] + } + groupedTemplates[catId].push(template) } - groupedTemplates[catId].push(template) }) } }) From a77b0370e3c96dd2b2007784472030fe1a670abe Mon Sep 17 00:00:00 2001 From: vaishali k Date: Fri, 9 Jan 2026 07:02:06 +0530 Subject: [PATCH 07/44] Resolving commnets --- controllers/v1/library/categories.js | 2 +- models/project-templates.js | 1 - module/library/categories/helper.js | 17 +++-------------- module/project/templates/helper.js | 12 +++++------- 4 files changed, 9 insertions(+), 23 deletions(-) diff --git a/controllers/v1/library/categories.js b/controllers/v1/library/categories.js index 5dec79c4..2de50cca 100644 --- a/controllers/v1/library/categories.js +++ b/controllers/v1/library/categories.js @@ -258,7 +258,7 @@ module.exports = class LibraryCategories extends Abstract { /** * delete a library category * @method - * @name list + * @name delete * @param {Object} req - requested data * @returns {Array} Library categories. */ diff --git a/models/project-templates.js b/models/project-templates.js index 2a003277..3e181fff 100644 --- a/models/project-templates.js +++ b/models/project-templates.js @@ -23,7 +23,6 @@ module.exports = { type: String, index: true, }, - name: String, }, ], description: { diff --git a/module/library/categories/helper.js b/module/library/categories/helper.js index 9da1377e..8eeb3df4 100644 --- a/module/library/categories/helper.js +++ b/module/library/categories/helper.js @@ -1017,7 +1017,7 @@ module.exports = class LibraryCategoriesHelper { if (params['currentOrgOnly']) { let currentOrgOnly = UTILS.convertStringToBoolean(params['currentOrgOnly']) if (currentOrgOnly) { - query['orgId'] = { $in: ['ALL', req.userDetails.userInformation.organizationId] } + query['orgId'] = { $in: ['ALL', organizationId] } } } // Handle parentId query param. Accepts: actual id, omitted, or the string 'null' (for root) @@ -1271,19 +1271,8 @@ module.exports = class LibraryCategoriesHelper { isDeleted: false, } - let children = await projectCategoriesQueries.categoryDocuments(childrenQuery, [ - 'externalId', - 'name', - 'icon', - 'updatedAt', - 'noOfProjects', - 'description', - 'keywords', - 'parentId', - 'hasChildCategories', - 'sequenceNumber', - 'metaInformation', - ]) + const skipFields = ['__v', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'] + let children = await projectCategoriesQueries.categoryDocuments(childrenQuery, 'all', skipFields) categoryData[0].children = children } diff --git a/module/project/templates/helper.js b/module/project/templates/helper.js index 259f70fb..2a62a4d2 100644 --- a/module/project/templates/helper.js +++ b/module/project/templates/helper.js @@ -81,10 +81,7 @@ module.exports = class ProjectTemplatesHelper { matchQuery['tenantId'] = userDetails.tenantAndOrgInfo.tenantId matchQuery['externalId'] = { $in: categoryIds } // what is category documents - let categories = await projectCategoriesQueries.categoryDocuments(matchQuery, [ - 'externalId', - 'name', - ]) + let categories = await projectCategoriesQueries.categoryDocuments(matchQuery, ['externalId']) if (!categories.length > 0) { throw { @@ -99,7 +96,6 @@ module.exports = class ProjectTemplatesHelper { [category.externalId]: { _id: ObjectId(category._id), externalId: category.externalId, - name: category.name, }, }), {} @@ -2063,10 +2059,12 @@ module.exports = class ProjectTemplatesHelper { } if (groupByCategory) { + let groupedTemplates = {} paginatedResults.forEach((template) => { if (template.categories && template.categories.length > 0) { - template.categories.forEach((categoryId) => { - const catId = categoryId.toString() // Convert ObjectId to string for key + template.categories.forEach((category) => { + let catId = category._id.toString() + if (categoryIdArray && categoryIdArray.length > 0) { // Check if the current categoryId is in the requestedCategorySet if (categoryIdArray.includes(catId)) { From a6b6f27ffca16b9d86da28dc645d0f569b4e6448 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Fri, 9 Jan 2026 07:07:11 +0530 Subject: [PATCH 08/44] Resolving commnets --- module/library/categories/helper.js | 31 ++++++++++++++++------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/module/library/categories/helper.js b/module/library/categories/helper.js index 8eeb3df4..2742f0c0 100644 --- a/module/library/categories/helper.js +++ b/module/library/categories/helper.js @@ -1087,22 +1087,25 @@ module.exports = class LibraryCategoriesHelper { // If getChildren is true, fetch immediate children for each category if (params.getChildren) { - for (let category of categoryData) { - let childrenQuery = { - parentId: category._id, - tenantId: tenantId, - status: CONSTANTS.common.ACTIVE_STATUS, - isDeleted: false, - } + let getChildren = UTILS.convertStringToBoolean(params['getChildren']) + if (getChildren) { + for (let category of categoryData) { + let childrenQuery = { + parentId: category._id, + tenantId: tenantId, + status: CONSTANTS.common.ACTIVE_STATUS, + isDeleted: false, + } - let children = await projectCategoriesQueries.categoryDocuments( - childrenQuery, - 'all', - skipFields - ) + let children = await projectCategoriesQueries.categoryDocuments( + childrenQuery, + 'all', + skipFields + ) - category.children = children - category.childrenCount = children.length + category.children = children + category.childrenCount = children.length + } } } From 785dbe903f75a60a11d3d033ed268ed097dad46f Mon Sep 17 00:00:00 2001 From: vaishali k Date: Fri, 9 Jan 2026 15:12:42 +0530 Subject: [PATCH 09/44] Resolving commnets --- models/project-categories.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/models/project-categories.js b/models/project-categories.js index 3ab91ccc..d664c699 100644 --- a/models/project-categories.js +++ b/models/project-categories.js @@ -68,7 +68,6 @@ module.exports = { }, description: { type: String, - index: true, required: true, default: 'default', }, @@ -102,11 +101,9 @@ module.exports = { name: { externalId: 1, tenantId: 1 }, indexType: { unique: true }, }, + // For Query (parentId + tenantId queries) { - name: { parentId: 1, tenantId: 1, sequenceNumber: 1 }, - }, - { - name: { tenantId: 1, hasChildCategories: 1 }, + name: { parentId: 1, tenantId: 1 }, }, ], } From 28fe71dce4c6994929a20186f02996ff76160e04 Mon Sep 17 00:00:00 2001 From: Abhilash Dubey <124042593+AbhilashKD@users.noreply.github.com> Date: Sat, 10 Jan 2026 11:35:50 +0530 Subject: [PATCH 10/44] Update .env.sample --- .env.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.sample b/.env.sample index 60d200e3..24844695 100644 --- a/.env.sample +++ b/.env.sample @@ -98,4 +98,4 @@ ORG_UPDATES_TOPIC = elevate_project_org_extension_event_listener USER_ACCOUNT_EVENT_TOPIC = elevate_user_account_event_listener // Kafka topic to listen user account events SESSION_VERIFICATION_METHOD = user_service_authenticated // session verification method -USER_SERVICE_INTERNAL_ACCESS_TOKEN_HEADER_KEY = internal_access_token // user service's internal access token header key \ No newline at end of file +USER_SERVICE_INTERNAL_ACCESS_TOKEN_HEADER_KEY = internal_access_token // user service's internal access token header key. From be237cf75bff13146f998a38a7dcd795b9ece357 Mon Sep 17 00:00:00 2001 From: Abhilash Dubey <124042593+AbhilashKD@users.noreply.github.com> Date: Sat, 10 Jan 2026 12:03:30 +0530 Subject: [PATCH 11/44] Create brac-dev-deployment.yaml --- .github/workflows/brac-dev-deployment.yaml | 77 ++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/workflows/brac-dev-deployment.yaml diff --git a/.github/workflows/brac-dev-deployment.yaml b/.github/workflows/brac-dev-deployment.yaml new file mode 100644 index 00000000..aa596956 --- /dev/null +++ b/.github/workflows/brac-dev-deployment.yaml @@ -0,0 +1,77 @@ +name: Dev Build & Deploy Project Service (BRAC) + +on: + push: + branches: + - develop + +env: + AWS_REGION: ${{ secrets.AWS_REGION }} + ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY_BRAC }} + AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # ========================= + # AWS Authentication + # ========================= + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + # ========================= + # Login to Amazon ECR + # ========================= + - name: Login to Amazon ECR + uses: aws-actions/amazon-ecr-login@v2 + + # ========================= + # Build & Push Image + # ========================= + - name: Build and Push Docker Image to ECR + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}:latest + ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}:${{ github.sha }} + + # ========================= + # Deploy on Server + # ========================= + - name: Deploy Stack + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.HOST_NAME_DEV }} + username: ${{ secrets.USERNAME_DEV }} + key: ${{ secrets.SSH_KEY_DEV }} + port: ${{ secrets.PORT_DEV }} + script: | + set -e + + cd ${{ secrets.TARGET_DIR_DEV }} + + if [ -f .env ]; then + mv .env .env-bkp + fi + + echo '${{ secrets.DEV_ENV_BRAC }}' > .env + + aws ecr get-login-password --region $AWS_REGION \ + | docker login --username AWS \ + --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com + + ./deploy.sh From 4883172b08b76aaaaf253c219234539cde5ed733 Mon Sep 17 00:00:00 2001 From: Abhilash Dubey <124042593+AbhilashKD@users.noreply.github.com> Date: Sat, 10 Jan 2026 12:09:24 +0530 Subject: [PATCH 12/44] Explicitly export AWS variables in deployment script Export AWS_REGION and AWS_ACCOUNT_ID variables explicitly before using them in the script. --- .github/workflows/brac-dev-deployment.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/brac-dev-deployment.yaml b/.github/workflows/brac-dev-deployment.yaml index aa596956..6e77ff09 100644 --- a/.github/workflows/brac-dev-deployment.yaml +++ b/.github/workflows/brac-dev-deployment.yaml @@ -62,6 +62,10 @@ jobs: script: | set -e + # Export AWS variables explicitly + export AWS_REGION="${{ secrets.AWS_REGION }}" + export AWS_ACCOUNT_ID="${{ secrets.AWS_ACCOUNT_ID }}" + cd ${{ secrets.TARGET_DIR_DEV }} if [ -f .env ]; then @@ -70,8 +74,8 @@ jobs: echo '${{ secrets.DEV_ENV_BRAC }}' > .env - aws ecr get-login-password --region $AWS_REGION \ + aws ecr get-login-password --region "$AWS_REGION" \ | docker login --username AWS \ - --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com + --password-stdin "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com" ./deploy.sh From 73288b4b01055a71a9f97a55dcfba81a846b9bf1 Mon Sep 17 00:00:00 2001 From: Abhilash Dubey <124042593+AbhilashKD@users.noreply.github.com> Date: Sat, 10 Jan 2026 12:13:28 +0530 Subject: [PATCH 13/44] Update brac-dev-deployment.yaml --- .github/workflows/brac-dev-deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/brac-dev-deployment.yaml b/.github/workflows/brac-dev-deployment.yaml index 6e77ff09..f390be26 100644 --- a/.github/workflows/brac-dev-deployment.yaml +++ b/.github/workflows/brac-dev-deployment.yaml @@ -46,7 +46,7 @@ jobs: context: . push: true tags: | - ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}:latest + ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}:latest-brac ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}:${{ github.sha }} # ========================= From c6880eb6b8eb8e8c54cd18cac5e1c8c9728e53b5 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Mon, 12 Jan 2026 11:26:10 +0530 Subject: [PATCH 14/44] Issue #000 fix: if entityId is passed to the importFromLibray, error is thrown in entity helper as tenant_id is required --- generics/services/entity-management.js | 1 + module/userProjects/helper.js | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/generics/services/entity-management.js b/generics/services/entity-management.js index e85801fd..491cc18b 100644 --- a/generics/services/entity-management.js +++ b/generics/services/entity-management.js @@ -65,6 +65,7 @@ const entityDocuments = function ( headers: { 'content-type': 'application/json', 'internal-access-token': process.env.INTERNAL_ACCESS_TOKEN, + 'tenant-id': filterData.tenantId ? filterData.tenantId : '', }, json: requestJSON, } diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 75e9f6a9..2135b0e0 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -1291,7 +1291,7 @@ module.exports = class UserProjectsHelper { "programId": "685140cbf891ccf74e05baf9", "observationId": "685146542054fe175c7150c8", "solutionId": "685140d1ffc25f705c56e99e", - } + } } */ @@ -3089,7 +3089,7 @@ module.exports = class UserProjectsHelper { // If it is valid make sure we add those data to newly creating projects if (requestedData.entityId && requestedData.entityId !== '') { let entityInformation = await entitiesService.entityDocuments( - { _id: requestedData.entityId }, + { _id: requestedData.entityId, tenantId: tenantId }, CONSTANTS.common.ALL ) @@ -4322,7 +4322,7 @@ module.exports = class UserProjectsHelper { * deleteUserPIIData function to delete users Data. * @method * @name deleteUserPIIData - * @param {userDeleteEvent} - userDeleteEvent message object + * @param {userDeleteEvent} - userDeleteEvent message object * { "entity": "user", "eventType": "delete", From 119f728cc4f6804b859ee038f7e8db843d99ac20 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Mon, 12 Jan 2026 11:56:34 +0530 Subject: [PATCH 15/44] Issue #000 fix: if entityId is passed to the importFromLibray, error is thrown in entity helper as tenant_id is required --- generics/services/entity-management.js | 1 - 1 file changed, 1 deletion(-) diff --git a/generics/services/entity-management.js b/generics/services/entity-management.js index 491cc18b..e85801fd 100644 --- a/generics/services/entity-management.js +++ b/generics/services/entity-management.js @@ -65,7 +65,6 @@ const entityDocuments = function ( headers: { 'content-type': 'application/json', 'internal-access-token': process.env.INTERNAL_ACCESS_TOKEN, - 'tenant-id': filterData.tenantId ? filterData.tenantId : '', }, json: requestJSON, } From ee6f92b8d159712c1c8f34bb36a52e309b6be0fb Mon Sep 17 00:00:00 2001 From: vaishali k Date: Tue, 20 Jan 2026 12:13:22 +0530 Subject: [PATCH 16/44] Issue #000 fix: ProgramUser mapping --- controllers/v1/programUsers.js | 83 +++++- databaseQueries/programUsers.js | 366 ++++++++++++++++++++++--- envVariables.js | 5 + generics/middleware/authenticator.js | 5 +- generics/services/programUsers.js | 220 +++++++++++++++ models/programUsers.js | 187 ++++++++++--- module/programUsers/helper.js | 384 ++++++++++++++++++++++++++- module/programUsers/validator/v1.js | 162 +++++++++++ 8 files changed, 1313 insertions(+), 99 deletions(-) create mode 100644 generics/services/programUsers.js create mode 100644 module/programUsers/validator/v1.js diff --git a/controllers/v1/programUsers.js b/controllers/v1/programUsers.js index 35d8ef63..36128b4b 100644 --- a/controllers/v1/programUsers.js +++ b/controllers/v1/programUsers.js @@ -2,21 +2,82 @@ * name : programUsers.js * author : Ankit Shahu * created-date : 9-Jan-2023 - * Description : PII data related controller. -*/ + * Description : Program Users related controller. + */ + +const programUsersHelper = require(MODULES_BASE_PATH + '/programUsers/helper') /** - * programUsers - * @class + * programUsers + * @class */ module.exports = class ProgramUsers extends Abstract { - constructor() { - super("programUsers"); - } + constructor() { + super('programUsers') + } + + static get name() { + return 'programUsers' + } + + /** + * Create or update program user + * Supports: create, update, add entity, update status + * @method + * @name createOrUpdate + * @param {Object} req - request object + * @returns {Object} response with status and result + */ + async createOrUpdate(req) { + return new Promise(async (resolve, reject) => { + try { + const result = await programUsersHelper.createOrUpdate(req.body, req.userDetails) - static get name() { - return "programUsers"; - } + return resolve(result) + } catch (error) { + return reject({ + status: HTTP_STATUS_CODE.internal_server_error.status, + message: error.message || 'Internal server error', + }) + } + }) + } -}; + /** + * Get entities from a program user with pagination and search + * @method + * @name getEntities + * @param {Object} req - request object + * @returns {Object} paginated list of entities + */ + async getEntities(req) { + return new Promise(async (resolve, reject) => { + try { + const { userId, programId, programExternalId, page = 1, limit = 20, search = '' } = req.query + if (!userId || (!programId && !programExternalId)) { + return reject({ + status: HTTP_STATUS_CODE.bad_request.status, + message: 'userId and either programId or programExternalId are required', + }) + } + // Call helper + const result = await programUsersHelper.getEntitiesWithPagination( + userId, + programId, + programExternalId, + parseInt(page), + parseInt(limit), + search, + req.userDetails + ) + return resolve(result) + } catch (error) { + return reject({ + status: HTTP_STATUS_CODE.internal_server_error.status, + message: error.message || 'Internal server error', + }) + } + }) + } +} diff --git a/databaseQueries/programUsers.js b/databaseQueries/programUsers.js index 479b8b8e..51cc1365 100644 --- a/databaseQueries/programUsers.js +++ b/databaseQueries/programUsers.js @@ -5,48 +5,326 @@ * Description : program users helper for DB interactions. */ module.exports = class programUsers { + /** + * program users details. + * @method + * @name programUsersDocument + * @param {Array} [filterData = "all"] - program users filter query. + * @param {Array} [fieldsArray = "all"] - projected fields. + * @param {Array} [skipFields = "none"] - field not to include + * @returns {Array} program users details. + */ - /** - * program users details. - * @method - * @name programUsersDocument - * @param {Array} [filterData = "all"] - program users filter query. - * @param {Array} [fieldsArray = "all"] - projected fields. - * @param {Array} [skipFields = "none"] - field not to include - * @returns {Array} program users details. - */ - - static programUsersDocument( - 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 programJoinedData = await database.models.programUsers - .find(queryObject, projection) - .lean(); - return resolve(programJoinedData); - } catch (error) { - return reject(error); - } - }); - } - -}; + static programUsersDocument(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 programJoinedData = await database.models.programUsers.find(queryObject, projection).lean() + return resolve(programJoinedData) + } catch (error) { + return reject(error) + } + }) + } + + /** + * Create or update program user record + * @method + * @name createOrUpdate + * @param {Object} filterData - filter criteria + * @param {Object} updateData - data to update/insert + * @param {Object} options - update options (upsert, new, etc) + * @returns {Object} program user document + */ + static createOrUpdate(filterData, updateData, options = {}) { + return new Promise(async (resolve, reject) => { + try { + const defaultOptions = { + upsert: true, + new: true, + setDefaultsOnInsert: true, + lean: true, + } + + const finalOptions = { ...defaultOptions, ...options } + + // Check if we need to handle hierarchy replacement (for existing documents) + const hierarchyReplacement = updateData._hierarchyReplacement + delete updateData._hierarchyReplacement + + // Check if we need to push entities (for existing documents) + const entitiesToPush = updateData._entitiesToPush + delete updateData._entitiesToPush + + // Check if we need to set metaInformation + const metaInformationToSet = updateData._metaInformationToSet + delete updateData._metaInformationToSet + + let result = await database.models.programUsers.findOneAndUpdate(filterData, updateData, finalOptions) + + // If metaInformation needs to be set, do it separately + if (metaInformationToSet && result && result._id) { + result = await database.models.programUsers.findOneAndUpdate( + { _id: result._id }, + { + $set: { metaInformation: metaInformationToSet, updatedAt: new Date() }, + }, + { new: true, lean: true } + ) + } + + // If entities need to be pushed and document exists, do push separately + if (entitiesToPush && result && result._id) { + // Fetch existing entities to check for duplicates + const existingDoc = await database.models.programUsers.findById(result._id, { entities: 1 }).lean() + const existingEntities = existingDoc?.entities || [] + const existingUserIds = new Set(existingEntities.map((e) => e.userId)) + + // Separate new entities from updates + const newEntities = [] + const entitiesToUpdate = [] + + entitiesToPush.forEach((entity) => { + if (existingUserIds.has(entity.userId)) { + entitiesToUpdate.push(entity) + } else { + newEntities.push(entity) + } + }) + + // Update existing entities + for (const entity of entitiesToUpdate) { + await database.models.programUsers.findOneAndUpdate( + { _id: result._id, 'entities.userId': entity.userId }, + { + $set: { + 'entities.$': entity, + updatedAt: new Date(), + }, + }, + { new: true, lean: true } + ) + } + + // Push only new entities + if (newEntities.length > 0) { + result = await database.models.programUsers.findOneAndUpdate( + { _id: result._id }, + { + $push: { entities: { $each: newEntities } }, + $set: { updatedAt: new Date() }, + }, + { new: true, lean: true } + ) + } + } + + // If hierarchy replacement is needed and document exists, do two separate updates + if (hierarchyReplacement && result && result._id) { + // First: Remove old entries at matching levels + const pullCondition = { + $or: hierarchyReplacement.map((item) => ({ level: item.level })), + } + + await database.models.programUsers.findOneAndUpdate( + { _id: result._id }, + { + $pull: { hierarchy: pullCondition }, + $set: { updatedAt: new Date() }, + }, + { new: true, lean: true } + ) + + // Second: Add new hierarchy items + result = await database.models.programUsers.findOneAndUpdate( + { _id: result._id }, + { + $push: { hierarchy: { $each: hierarchyReplacement } }, + $set: { updatedAt: new Date() }, + }, + { new: true, lean: true } + ) + } + + return resolve(result) + } catch (error) { + return reject(error) + } + }) + } + + /** + * Add entity to entities array + * @method + * @name addEntity + * @param {String} docId - document _id + * @param {Object} entity - entity object to add + * @returns {Object} updated document + */ + static addEntity(docId, entity) { + return new Promise(async (resolve, reject) => { + try { + let result = await database.models.programUsers.findByIdAndUpdate( + docId, + { + $push: { entities: entity }, + $set: { + updatedAt: new Date(), + 'overview.assigned': { + $cond: [ + { $eq: ['$overview.assigned', undefined] }, + 1, + { $add: ['$overview.assigned', 1] }, + ], + }, + }, + }, + { new: true, lean: true } + ) + + return resolve(result) + } catch (error) { + return reject(error) + } + }) + } + + /** + * Update entity in entities array + * @method + * @name updateEntity + * @param {String} docId - document _id + * @param {String} entityUserId - entity user id + * @param {Object} updateData - data to update in entity + * @returns {Object} updated document + */ + static updateEntity(docId, entityUserId, updateData) { + return new Promise(async (resolve, reject) => { + try { + const updateObj = {} + Object.keys(updateData).forEach((key) => { + updateObj[`entities.$.${key}`] = updateData[key] + }) + + let result = await database.models.programUsers.findOneAndUpdate( + { _id: docId, 'entities.userId': entityUserId }, + { + $set: { + ...updateObj, + updatedAt: new Date(), + }, + }, + { new: true, lean: true } + ) + + return resolve(result) + } catch (error) { + return reject(error) + } + }) + } + + /** + * Update overview statistics + * @method + * @name updateOverview + * @param {String} docId - document _id + * @param {Object} updateOperations - increment/decrement operations + * @returns {Object} updated document + */ + static updateOverview(docId, updateOperations) { + return new Promise(async (resolve, reject) => { + try { + const updateObj = { $set: { 'overview.lastRecalculated': new Date() } } + + if (updateOperations.$inc) { + updateObj.$inc = updateOperations.$inc + } + if (updateOperations.$set) { + updateObj.$set = { ...updateObj.$set, ...updateOperations.$set } + } + + let result = await database.models.programUsers.findByIdAndUpdate(docId, updateObj, { + new: true, + lean: true, + }) + + return resolve(result) + } catch (error) { + return reject(error) + } + }) + } + + /** + * Get document with entities and pagination + * @method + * @name getDocumentWithEntities + * @param {String} docId - document _id + * @param {Number} skip - skip count for pagination + * @param {Number} limit - limit for pagination + * @param {String} searchQuery - search text + * @returns {Object} document with entities + */ + static getDocumentWithEntities(docId, skip = 0, limit = 20, searchQuery = '') { + return new Promise(async (resolve, reject) => { + try { + let result = await database.models.programUsers + .findById(docId, { + entities: 1, + overview: 1, + userId: 1, + programId: 1, + }) + .lean() + + return resolve(result) + } catch (error) { + return reject(error) + } + }) + } + + /** + * Find program user by userId and either programId or programExternalId + * @method + * @name findByUserAndProgram + * @param {String} userId - user ID + * @param {String} programId - program ID (optional) + * @param {String} programExternalId - program external ID (optional) + * @returns {Object} program user document + */ + static findByUserAndProgram(userId, programId, programExternalId) { + return new Promise(async (resolve, reject) => { + try { + // Build query: use programId if available, otherwise programExternalId + const query = { userId } + if (programId) { + query.programId = programId + } else if (programExternalId) { + query.programExternalId = programExternalId + } + + let result = await database.models.programUsers.findOne(query).lean() + + return resolve(result) + } catch (error) { + return reject(error) + } + }) + } +} diff --git a/envVariables.js b/envVariables.js index a17ef042..b51b417a 100644 --- a/envVariables.js +++ b/envVariables.js @@ -245,6 +245,11 @@ let enviromentVariables = { optional: true, default: 'internal_access_token', }, + PROGRAM_USERS_ENTITIES: { + message: 'The program users entities type', + optional: true, + default: 'users', + }, } let success = true diff --git a/generics/middleware/authenticator.js b/generics/middleware/authenticator.js index 33b9a9a8..aa4841a4 100644 --- a/generics/middleware/authenticator.js +++ b/generics/middleware/authenticator.js @@ -75,6 +75,7 @@ module.exports = async function (req, res, next, token = '') { '/scp/publishTemplateAndTasks', '/library/categories/create', '/library/categories/update', + '/library/categories/delete', '/programs/create', '/programs/update', '/programs/read', @@ -112,6 +113,7 @@ module.exports = async function (req, res, next, token = '') { '/organizationExtension/updateRelatedOrgs', '/userExtension/update', '/solutions/fetchLinkInternal', + '/programUsers/createOrUpdate', ] let performInternalAccessTokenCheck = false let adminHeader = false @@ -138,7 +140,6 @@ module.exports = async function (req, res, next, token = '') { return } } - if (!token) { rspObj.errCode = CONSTANTS.apiResponses.TOKEN_MISSING_CODE rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE @@ -165,7 +166,7 @@ module.exports = async function (req, res, next, token = '') { rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status // <---- For Elevate user service user compactibility ----> - let decodedToken = null + let errdecodedToken = null let userInformation = {} try { if (process.env.AUTH_METHOD === CONSTANTS.common.AUTH_METHOD.NATIVE) { diff --git a/generics/services/programUsers.js b/generics/services/programUsers.js new file mode 100644 index 00000000..9d4e133f --- /dev/null +++ b/generics/services/programUsers.js @@ -0,0 +1,220 @@ +/** + * name : programUsers.js + * description : Program Users service + * created-date : 19-Jan-2026 + */ + +const programUsersQueries = require(DB_QUERY_BASE_PATH + '/programUsers') +const userService = require(GENERICS_FILES_PATH + '/services/users') + +module.exports = class ProgramUsersService { + /** + * Create or update program user - handles all operations in a single update + * @method + * @name createOrUpdate + * @param {Object} data - request data with all parameters + * @returns {Object} result with status and data + */ + static async createOrUpdate(data) { + try { + const { + tenantId, + orgId, + userId, + programId, + programExternalId, + hierarchy = [], + entities = [], + status, + metaInformation = {}, + } = data + + // Build filter based on available identifiers + let filterData = {} + if (userId) { + filterData.userId = userId + } + if (programId) { + filterData.programId = programId + } else if (programExternalId) { + filterData.programExternalId = programExternalId + } + + // Build update data with all provided parameters + const updateData = { + $set: { + updatedAt: new Date(), + tenantId, + orgId, + }, + $setOnInsert: { + createdAt: new Date(), + overview: {}, + }, + } + + // Set base fields if provided + if (userId) { + updateData.$set.userId = userId + } + if (status) { + updateData.$set.status = status + } + if (programId) { + updateData.$set.programId = programId + } + if (programExternalId) { + updateData.$set.programExternalId = programExternalId + } + + // Handle hierarchy: replace if same level exists, else add + if (hierarchy && hierarchy.length > 0) { + // For new documents: just set the hierarchy + updateData.$setOnInsert.hierarchy = hierarchy + + // For existing documents: remove old levels and push new ones + // Split into two operations if document exists + if (hierarchy.length > 0) { + // Store hierarchy for special handling + updateData._hierarchyReplacement = hierarchy + } + } else { + updateData.$setOnInsert.hierarchy = [] + } + + // Handle entities: set on insert, push on update + if (entities && entities.length > 0) { + updateData.$setOnInsert.entities = entities + // Store entities for special handling in query layer + updateData._entitiesToPush = entities + } else { + updateData.$setOnInsert.entities = [] + } + + // Handle metaInformation: store for query layer to handle separately + // This includes both user-provided keys and entityDetails to avoid MongoDB conflicts + const metaInformationToSet = { ...metaInformation } + metaInformationToSet.entityDetails = { + service: process.env.PROGRAM_USERS_ENTITY_SERVICE, + endPoint: process.env.PROGRAM_USERS_ENTITY_END_POINT, + } + updateData._metaInformationToSet = metaInformationToSet + + const result = await programUsersQueries.createOrUpdate(filterData, updateData, { + upsert: true, + new: true, + lean: true, + }) + delete result.overview // Remove overview from result + return { + status: result.createdAt === result.updatedAt ? 201 : 200, + message: 'Program user created or updated successfully', + result, + } + } catch (error) { + throw error + } + } + + /** + * Update overview statistics + * @method + * @name updateOverview + * @param {String} docId - document _id + * @param {Object} operations - increment/decrement operations + * @returns {Object} updated document + */ + static async updateOverview(docId, operations) { + try { + const result = await programUsersQueries.updateOverview(docId, operations) + return result + } catch (error) { + throw error + } + } + + /** + * Get program user entities with pagination + * @method + * @name getEntitiesWithPagination + * @param {String} userId - user ID + * @param {String} programId - program ID (optional) + * @param {String} programExternalId - program external ID (optional) + * @param {Number} page - page number + * @param {Number} limit - items per page + * @param {String} searchQuery - search text + * @returns {Object} entities with pagination info + */ + static async getEntitiesWithPagination( + userId, + programId, + programExternalId, + page = 1, + limit = 20, + searchQuery = '', + userDetails + ) { + try { + const skip = (page - 1) * limit + + // Find document by userId and either programId or programExternalId + const docData = await this.findByUserAndProgram(userId, programId, programExternalId) + + if (!docData) { + return { + status: 404, + message: 'Program user not found', + data: [], + } + } + + // Get entities from the found document + let filteredEntities = docData.entities || [] + // Apply pagination + const totalCount = filteredEntities.length + const paginatedEntities = filteredEntities.slice(skip, skip + limit) + + const userIds = paginatedEntities.map((entity) => entity.userId).filter(Boolean) + // if (process.env.PROGRAM_USERS_ENTITIES === 'users') { + // Fetch user details from user service + const { success, data } = + (await userService.accountSearch(userIds, userDetails.userInformation.tenantId)) || {} + // Throw error if no valid users returned from service + if (!success || !data || data.count === 0) { + throw { + success: false, + status: HTTP_STATUS_CODE.bad_request.status, + message: 'No valid users found for the provided entity user IDs.', + } + } + return { + status: 200, + message: 'Entities retrieved successfully', + data: { data: data.data, overview: docData.overview || {} }, + result: { data: data.data, overview: docData.overview || {} }, + count: data.count, + total: totalCount, + } + } catch (error) { + throw error + } + } + + /** + * Find program user by userId and either programId or programExternalId + * @method + * @name findByUserAndProgram + * @param {String} userId - user ID + * @param {String} programId - program ID (optional) + * @param {String} programExternalId - program external ID (optional) + * @returns {Object} program user document + */ + static async findByUserAndProgram(userId, programId, programExternalId) { + try { + const result = await programUsersQueries.findByUserAndProgram(userId, programId, programExternalId) + return result + } catch (error) { + throw error + } + } +} diff --git a/models/programUsers.js b/models/programUsers.js index b276a3b8..7b8cdb43 100644 --- a/models/programUsers.js +++ b/models/programUsers.js @@ -1,38 +1,151 @@ +/** + * Hierarchy Schema + * Stores the management chain (who reports to whom) + */ +const hierarchySchema = { + level: { + type: Number, + required: true, + min: 0, + description: "0 = direct manager, 1 = manager's manager, etc.", + }, + id: { + type: String, + required: true, + description: 'User ID of the manager (LC, Supervisor, etc.)', + }, +} + module.exports = { - name: "programUsers", - schema: { - programId: { - type : "ObjectId", - required: true, - index: true - }, - userId: { - type: String, - index: true, - required: true, - }, - resourcesStarted: { - type: Boolean, - index: true, - default: false - }, - userProfile: { - type : Object, - required: true - }, - userRoleInformation: Object, - appInformation: Object, - consentShared: { - type: Boolean, - default: false - } - }, - compoundIndex: [ - { - "name" :{ userId: 1, programId: 1 }, - "indexType" : { unique: true } - } - ] -}; - - \ No newline at end of file + name: 'programUsers', + schema: { + // ============================================ + // CORE IDENTIFIERS + // ============================================ + userId: { + type: String, + required: true, + index: true, + description: 'User ID', + }, + programId: { + type: String, + required: true, + index: true, + description: 'Program ID', + }, + programExternalId: { + type: String, + required: true, + description: 'Human-readable external program identifier', + }, + + // ============================================ + // HIERARCHY & RELATIONSHIPS + // ============================================ + hierarchy: { + type: [hierarchySchema], + default: [], + description: "Management hierarchy (level 0 = direct manager, level 1 = manager's manager, etc.)", + }, + + // ============================================ + // OVERVIEW (FOR LC ONLY) + // ============================================ + overview: { + type: Object, + default: () => ({}), + description: 'Summary statistics (used only for LC programs, empty object for participants)', + }, + + // ============================================ + // ENTITIES (FOR LC ONLY - MATERIALIZED VIEW) + // ============================================ + entities: { + type: [Object], + default: [], + description: 'List of entities (participants) a user is managing', + }, + + // ============================================ + // METADATA & REFERENCES + // ============================================ + referenceFrom: { + type: Object, + description: 'Reference to parent/global program (for participants only)', + }, + + metaInformation: { + type: Object, + default: () => ({}), + description: 'Additional context and timeline data', + }, + + // ============================================ + // TAGS (OPTIONAL - FOR FILTERING/SEARCH) + // ============================================ + tags: { + type: [String], + default: [], + description: 'Optional tags for categorization and search', + }, + + // ============================================ + // STATUS + // ============================================ + status: { + type: String, + required: true, + enum: [ + // For LC/Supervisor + 'ACTIVE', + 'INACTIVE', + // For Participant + 'NOT_ONBOARDED', + 'ONBOARDED', + 'IN_PROGRESS', + 'COMPLETED', + 'GRADUATED', + 'DROPPED_OUT', + ], + index: true, + description: 'Current status of program user', + default: 'ACTIVE', + }, + tenantId: { + type: String, + index: true, + required: true, + }, + orgId: { + type: String, + index: true, + required: true, + }, + // ============================================ + // AUDIT FIELDS + // ============================================ + createdBy: { + type: String, + required: true, + description: 'User ID who created this entry', + }, + updatedBy: { + type: String, + required: true, + description: 'User ID who last updated this entry', + }, + }, + compoundIndex: [ + { + name: { userId: 1, programId: 1 }, + indexType: { unique: true }, + }, + { + name: { programId: 1, status: 1 }, + }, + { + name: { 'hierarchy.id': 1 }, + }, + ], +} diff --git a/module/programUsers/helper.js b/module/programUsers/helper.js index 809a496f..740d741e 100644 --- a/module/programUsers/helper.js +++ b/module/programUsers/helper.js @@ -5,25 +5,26 @@ * Description : Programs users related helper functionality. */ +const { resolveLevel } = require('bunyan') + // Dependencies +const programUsersQueries = require(DB_QUERY_BASE_PATH + '/programUsers') +const programUsersService = require(SERVICES_BASE_PATH + '/programUsers') +const programQueries = require(DB_QUERY_BASE_PATH + '/programs') /** * ProgramUsersHelper * @class */ - -const programUsersQueries = require(DB_QUERY_BASE_PATH + '/programUsers') - module.exports = class ProgramUsersHelper { /** * check if user joined a program or not and consentShared - * @method + * @methodcreateRecord * @name checkForUserJoinedProgramAndConsentShared * @param {String} programId - Program Id. * @param {String} userId - User Id * @returns {Object} result. */ - static checkForUserJoinedProgramAndConsentShared(programId, userId) { return new Promise(async (resolve, reject) => { try { @@ -43,4 +44,377 @@ module.exports = class ProgramUsersHelper { } }) } + + /** + * Create or update program user + * Handles all operations: create user, add entity, create record, update status + * @method + * @name createOrUpdate + * @param {Object} data - request body data containing all parameters + * @param {Object} userDetails - logged in user details + * @returns {Object} result with status and data + */ + static async createOrUpdate(data, userDetails) { + try { + const { + userId, + programId, + programExternalId, + entities, + hierarchy, + status, + metaInformation, + referenceFrom, + } = data + const loggedInUserId = userDetails.userInformation?.userId + const tenantId = userDetails.userInformation?.tenantId + const orgId = userDetails.userInformation?.organizationId + + // Validate required fields + if (!userId || (!programId && !programExternalId)) { + return { + success: false, + status: HTTP_STATUS_CODE.bad_request.status, + message: 'userId and either programId or programExternalId are required', + } + } + + // Check if program exists for given programId or programExternalId + const programExists = await this.checkProgramExists(tenantId, programId, programExternalId) + if (!programExists) { + return { + success: false, + status: HTTP_STATUS_CODE.not_found.status, + message: 'Program not found', + } + } + + // Validate referenceFrom if provided + if (referenceFrom) { + const referenceProgramExists = await this.checkProgramExists(tenantId, referenceFrom, null) + if (!referenceProgramExists || referenceProgramExists?.isAPrivateProgram) { + return { + success: false, + status: HTTP_STATUS_CODE.not_found.status, + message: 'Reference program not found OR is a private program', + } + } + + servicePayload.referenceFrom = referenceFrom + } + + // Prepare service payload with all parameters + const servicePayload = { + tenantId, + orgId, + userId, + programId, + programExternalId, + status, + metaInformation, + createdBy: loggedInUserId, + updatedBy: loggedInUserId, + } + + // Add entities if provided + if (entities && entities.length > 0) { + servicePayload.entities = entities + } + + // Add hierarchy if provided + if (hierarchy && Array.isArray(hierarchy) && hierarchy.length > 0) { + servicePayload.hierarchy = hierarchy + } + + // Call service with all parameters in single operation + const result = await programUsersService.createOrUpdate(servicePayload) + + // // Determine activity type for logging + // let activityType = 'STATUS_CHANGED'; + // if (entity) { + // activityType = 'ENTITY_ADDED'; + // } else if (referenceFrom) { + // activityType = 'RECORD_CREATED'; + // } else if (hierarchy && hierarchy.length > 0) { + // activityType = 'USER_CREATED'; + // } + + // // Log activity asynchronously (non-blocking) + // this.logActivity(activityType, result.result._id, { + // userId, + // programId, + // ...(entity && { entityId: entity.entityId, parentUserId: userId }), + // ...(referenceFrom && { parentProgramUsersId: referenceFrom }), + // ...(hierarchy && { hierarchyAdded: hierarchy.length }), + // ...(status && { newStatus: status }) + // }, loggedInUserId).catch(err => console.error('Activity logging error:', err)); + + // // Publish event asynchronously (non-blocking) + // this.publishEvent(activityType, { + // userId, + // programId, + // ...(entity && { entityUserId: entity.userId, entityId: entity.entityId }), + // ...(referenceFrom && { parentProgramUsersId: referenceFrom }), + // ...(status && { newStatus: status }), + // timestamp: new Date() + // }).catch(err => console.error('Event publishing error:', err)); + + // Update overview asynchronously (non-blocking) + if (result.result && result.result._id) { + setImmediate(async () => { + try { + const entities = result.result.entities || [] + // Count entities by status + const statusCounts = { + onboarded: 0, + inprogress: 0, + completed: 0, + graduated: 0, + droppedout: 0, + total: entities.length, + } + + entities.forEach((entity) => { + switch (entity.status) { + case 'ONBOARDED': + statusCounts.onboarded++ + break + case 'IN_PROGRESS': + statusCounts.inprogress++ + break + case 'COMPLETED': + statusCounts.completed++ + break + case 'GRADUATED': + statusCounts.graduated++ + break + case 'DROP_OUT': + statusCounts.droppedout++ + break + } + }) + + // Update overview with status counts + programUsersService + .updateOverview(result.result._id, { + $set: { + 'overview.assigned': statusCounts.total, + 'overview.onboarded': statusCounts.onboarded, + 'overview.inprogress': statusCounts.inprogress, + 'overview.completed': statusCounts.completed, + 'overview.graduated': statusCounts.graduated, + 'overview.droppedout': statusCounts.droppedout, + 'overview.lastModified': new Date(), + }, + }) + .catch((err) => console.error('Overview update error:', err)) + } catch (err) { + console.error('Error calculating entity counts:', err) + } + }) + } + + return { + success: true, + status: result.status, + message: result.message, + data: result.result, + result: result.result, + } + } catch (error) { + return { + success: false, + status: HTTP_STATUS_CODE.internal_server_error.status, + message: error.message || 'Internal server error', + } + } + } + + /** + * Get entities with pagination + * @method + * @name getEntitiesWithPagination + * @param {String} userId - user id + * @param {String} programId - program id + * @param {String} programExternalId - program external id + * @param {Number} page - page number + * @param {Number} limit - items per page + * @param {String} search - search query + * @returns {Object} result + */ + static async getEntitiesWithPagination( + userId, + programId, + programExternalId, + page = 1, + limit = 20, + search = '', + userDetails + ) { + try { + // Call service + const result = await programUsersService.getEntitiesWithPagination( + userId, + programId, + programExternalId, + page, + limit, + search, + userDetails + ) + + return { + success: true, + status: result.status, + message: result.message, + data: result.data, + result: result.data, + count: result.count, + total: result.total, + overview: result.overview, + } + } catch (error) { + return { + success: false, + status: HTTP_STATUS_CODE.internal_server_error.status, + message: error.message || 'Internal server error', + } + } + } + + /** + * Find program user + * @method + * @name findByUserAndProgram + * @param {String} userId - user id + * @param {String} programId - program id + * @param {String} programExternalId - program external id + * @returns {Object} program user document + */ + static async findByUserAndProgram(userId, programId, programExternalId) { + try { + return await programUsersService.findByUserAndProgram(userId, programId, programExternalId) + } catch (error) { + throw error + } + } + + /** + * Log activity to activity collection + * @method + * @name logActivity + * @param {String} activityType - type of activity (CREATE, UPDATE, ADD_ENTITY, etc) + * @param {String} programUsersRef - reference to programUsers document ID + * @param {Object} activityDetails - details of activity + * @param {String} createdBy - user who triggered activity + * @returns {Promise} void + */ + static async logActivity(activityType, programUsersRef, activityDetails, createdBy) { + try { + // Check if activity logging is enabled in config + const activityConfig = global.config?.activityConfig?.[activityType] + + if (!activityConfig?.enabled) { + return // Activity logging disabled for this type + } + + // Create activity log entry + const activity = { + programUsersRef, + activityType, + activityDetails, + createdBy, + createdAt: new Date(), + } + + // Insert into activity collection + // Uncomment when programUsersActivities model is available + // await database.models.programUsersActivities.create(activity); + + console.log('[ProgramUsers Activity]', activityType, activity) + } catch (error) { + // Don't throw - activity logging should not block main flow + console.error('[ProgramUsers Activity Error]', error.message) + } + } + + /** + * Publish event to Kafka message queue + * @method + * @name publishEvent + * @param {String} eventType - type of event + * @param {Object} eventData - event data payload + * @returns {Promise} void + */ + static async publishEvent(eventType, eventData) { + try { + // Get kafka producer from global or environment + const kafkaProducer = global.kafkaProducer || global.kafkaClient?.producer + + if (!kafkaProducer) { + console.log('[ProgramUsers Event] Kafka producer not available, skipping event:', eventType) + return + } + + // Determine topic from environment or event type + const topicName = process.env[`PROGRAMUSERS_${eventType}_TOPIC`] || 'program-users-events' + + const kafkaMessage = { + key: eventData.userId || eventData.programId, + value: JSON.stringify({ + eventType, + eventData, + timestamp: new Date().toISOString(), + service: 'project-service', + }), + } + + // Send to Kafka (implementation depends on kafka client version) + await kafkaProducer.send({ + topic: topicName, + messages: [kafkaMessage], + }) + + console.log('[ProgramUsers Event Published]', eventType, 'to', topicName) + } catch (error) { + // Don't throw - event publishing should not block main flow + console.error('[ProgramUsers Event Error]', eventType, error.message) + } + } + + /** + * Check if program exists by programId or programExternalId + * @method + * @name checkProgramExists + * @param {String} programId - program ID + * @param {String} programExternalId - program external ID + * @returns {Boolean} true if program exists, false otherwise + */ + static checkProgramExists(tenantId, programId, programExternalId) { + return new Promise(async (resolve, reject) => { + try { + let programMatchQuery = {} + programMatchQuery['tenantId'] = tenantId + + if (programId) { + programMatchQuery['_id'] = programId + } else { + programMatchQuery['externalId'] = programExternalId + } + + let programData = await programQueries.programsDocument(programMatchQuery, [ + 'name', + 'externalId', + 'isAPrivateProgram', + ]) + if (programData && programData.length > 0) { + return resolve(programData[0]) + } else { + return resolve(false) + } + } catch (error) { + return reject(error) + } + }) + } } diff --git a/module/programUsers/validator/v1.js b/module/programUsers/validator/v1.js new file mode 100644 index 00000000..3c927b63 --- /dev/null +++ b/module/programUsers/validator/v1.js @@ -0,0 +1,162 @@ +/** + * name : v1.js + * author : Ankit Shahu + * created-date : 9-Jan-2023 + * Description : Program Users validator. + */ + +module.exports = (req) => { + let programUsersValidator = { + createOrUpdate: function () { + // Validate required fields + req.checkBody('userId') + .exists() + .withMessage('userId is required') + .isString() + .withMessage('userId must be a string') + + // Either programId or programExternalId is required + req.checkBody('programId') + .custom((value) => { + const hasId = req.body.programId || req.body.programExternalId + if (!hasId) { + throw new Error('Either programId or programExternalId is required') + } + return true + }) + .optional() + + // Validate entity if provided + req.checkBody('entity') + .custom((value) => { + if (value) { + if (typeof value !== 'object' || Array.isArray(value)) { + throw new Error('entity must be an object') + } + if (!value.userId) { + throw new Error('entity.userId is required') + } + if (!value.entityId) { + throw new Error('entity.entityId is required') + } + } + return true + }) + .optional() + + // Validate referenceFrom if provided + req.checkBody('referenceFrom') + .custom((value) => { + if (value && (typeof value !== 'object' || Array.isArray(value))) { + throw new Error('referenceFrom must be an object') + } + return true + }) + .optional() + + // Validate hierarchy if provided + req.checkBody('hierarchy') + .custom((value) => { + if (value) { + if (!Array.isArray(value)) { + throw new Error('hierarchy must be an array') + } + value.forEach((h, index) => { + if (typeof h.level !== 'number' || h.level < 0) { + throw new Error(`hierarchy[${index}].level must be a non-negative number`) + } + if (!h.id || typeof h.id !== 'string') { + throw new Error(`hierarchy[${index}].id is required and must be a string`) + } + }) + } + return true + }) + .optional() + + // Validate status if provided + req.checkBody('status') + .custom((value) => { + if (value) { + const validStatuses = [ + 'ACTIVE', + 'INACTIVE', + 'NOT_ONBOARDED', + 'ONBOARDED', + 'IN_PROGRESS', + 'COMPLETED', + 'GRADUATED', + 'DROPPED_OUT', + ] + if (!validStatuses.includes(value)) { + throw new Error(`Invalid status. Must be one of: ${validStatuses.join(', ')}`) + } + } + return true + }) + .optional() + + // Validate metaInformation if provided + req.checkBody('metaInformation') + .custom((value) => { + if (value && (typeof value !== 'object' || Array.isArray(value))) { + throw new Error('metaInformation must be an object') + } + return true + }) + .optional() + }, + + getEntities: function () { + // Validate required query parameters + req.checkQuery('userId') + .exists() + .withMessage('userId is required') + .isString() + .withMessage('userId must be a string') + + // Either programId or programExternalId is required + req.checkQuery('programId') + .custom((value) => { + const hasId = req.query.programId || req.query.programExternalId + if (!hasId) { + throw new Error('Either programId or programExternalId is required') + } + return true + }) + .optional() + + // Validate pagination parameters + req.checkQuery('page') + .custom((value) => { + if (value) { + const pageNum = parseInt(value) + if (isNaN(pageNum) || pageNum < 1) { + throw new Error('page must be a positive number') + } + } + return true + }) + .optional() + + req.checkQuery('limit') + .custom((value) => { + if (value) { + const limitNum = parseInt(value) + if (isNaN(limitNum) || limitNum < 1) { + throw new Error('limit must be a positive number') + } + } + return true + }) + .optional() + + // Validate search if provided + req.checkQuery('search').isString().withMessage('search must be a string').optional() + }, + } + + if (programUsersValidator[req.params.method]) { + programUsersValidator[req.params.method]() + } +} From df605d89e415009a561f52ffd4374166f95e155a Mon Sep 17 00:00:00 2001 From: vaishali k Date: Tue, 20 Jan 2026 12:28:36 +0530 Subject: [PATCH 17/44] Routes added --- .../elevate-project/configs.json | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/constants/interface-routes/elevate-project/configs.json b/constants/interface-routes/elevate-project/configs.json index 18b611e3..8927b312 100644 --- a/constants/interface-routes/elevate-project/configs.json +++ b/constants/interface-routes/elevate-project/configs.json @@ -905,6 +905,34 @@ ], "service": "project" }, + { + "sourceRoute": "/project/v1/programUsers/createOrUpdate", + "type": "POST", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "project", + "packageName": "elevate-project" + } + ], + "service": "project" + }, + { + "sourceRoute": "/project/v1/programUsers/getEntities", + "type": "GET", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "project", + "packageName": "elevate-project" + } + ], + "service": "project" + }, { "sourceRoute": "/project/v1/programs/create", "type": "POST", From 90ab39364060ca819dfe3e09fdbfb07feffa0e66 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Tue, 20 Jan 2026 12:30:49 +0530 Subject: [PATCH 18/44] Routes added --- .../elevate-project/configs.json | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/constants/interface-routes/elevate-project/configs.json b/constants/interface-routes/elevate-project/configs.json index 8927b312..ca745443 100644 --- a/constants/interface-routes/elevate-project/configs.json +++ b/constants/interface-routes/elevate-project/configs.json @@ -905,6 +905,34 @@ ], "service": "project" }, + { + "sourceRoute": "/project/v1/library/categories/delete/:id", + "type": "DELETE", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "project", + "packageName": "elevate-project" + } + ], + "service": "project" + }, + { + "sourceRoute": "/project/v1/library/categories/details/:id", + "type": "GET", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "project", + "packageName": "elevate-project" + } + ], + "service": "project" + }, { "sourceRoute": "/project/v1/programUsers/createOrUpdate", "type": "POST", From af22646af3cbcc71a92fc3e56ffb2692bf5d4d6a Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:43:02 +0530 Subject: [PATCH 19/44] Issue#252622 Feat: Importing project from multiple templates --- .../elevate-project/configs.json | 14 + .../sunbird-project/configs.json | 14 + controllers/v1/userProjects.js | 68 ++++ generics/constants/api-responses.js | 1 + module/userProjects/helper.js | 383 ++++++++++++++++++ module/userProjects/validator/v1.js | 56 +++ 6 files changed, 536 insertions(+) diff --git a/constants/interface-routes/elevate-project/configs.json b/constants/interface-routes/elevate-project/configs.json index 18b611e3..c7eada39 100644 --- a/constants/interface-routes/elevate-project/configs.json +++ b/constants/interface-routes/elevate-project/configs.json @@ -1521,6 +1521,20 @@ ], "service": "project" }, + { + "sourceRoute": "/project/v1/userProjects/createProjectPlan", + "type": "POST", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "project", + "packageName": "elevate-project" + } + ], + "service": "project" + }, { "sourceRoute": "/project/v1/userProjects/update", "type": "POST", diff --git a/constants/interface-routes/sunbird-project/configs.json b/constants/interface-routes/sunbird-project/configs.json index 50f55856..4f331724 100644 --- a/constants/interface-routes/sunbird-project/configs.json +++ b/constants/interface-routes/sunbird-project/configs.json @@ -1406,6 +1406,20 @@ ], "service": "project" }, + { + "sourceRoute": "/project/v1/userProjects/createProjectPlan", + "type": "POST", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "project", + "packageName": "sunbird-project" + } + ], + "service": "project" + }, { "sourceRoute": "/project/v1/userProjects/update", "type": "POST", diff --git a/controllers/v1/userProjects.js b/controllers/v1/userProjects.js index 011fcef9..b99d7cd1 100644 --- a/controllers/v1/userProjects.js +++ b/controllers/v1/userProjects.js @@ -1545,6 +1545,74 @@ module.exports = class UserProjects extends Abstract { } */ + /** + * @api {post} /project/v1/userProjects/createProjectPlan + * @apiVersion 1.0.0 + * @apiGroup User Projects + * @apiName createProjectPlan + * @apiParamExample {json} Request: + * { + * "templates": [ + * { + * "templateId": "5f5b32cef16777642d51aaf0", + * "targetTaskName": "Social Empowerment", + * "customTasks": [] + * } + * ], + * "userId": "participantId", + * "entityId": "participantEntityId", + * "programName": "Program Name", + * "projectConfig": { + * "name": "IDP Name", + * "description": "IDP Description" + * } + * } + * @apiSuccessExample {json} Response: + * { + * "message": "Project Plan created successfully", + * "status": 200, + * "result": { + * "projectId": "master-project-id" + * } + * } + * @apiUse successBody + * @apiUse errorBody + */ + /** + * Create project plan. + * @method + * @name createProjectPlan + * @param {Object} req - request data. + * @returns {JSON} Project Plan created successfully. + */ + async createProjectPlan(req) { + return new Promise(async (resolve, reject) => { + try { + // Check if userDetails is properly set by authentication middleware + if (!req.userDetails || !req.userDetails.userInformation) { + return resolve({ + status: HTTP_STATUS_CODE.unauthorized.status, + message: 'Authentication failed - user details not available', + }) + } + let result = await userProjectsHelper.createProjectPlan( + req.body, + req.userDetails.userInformation.userId, + req.userDetails.userToken, + req.userDetails + ) + + return resolve(result) + } 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, + }) + } + }) + } + /** * Add entities in project. * @method diff --git a/generics/constants/api-responses.js b/generics/constants/api-responses.js index 216e3eb3..fdc8e25a 100644 --- a/generics/constants/api-responses.js +++ b/generics/constants/api-responses.js @@ -246,6 +246,7 @@ module.exports = { TASK_MANDATORY_FIELDS_MISSING: 'Task mandatoru fields missing', PROGRAM_NOT_FOUND: 'Program not found', PROJECTS_CREATED: 'Projects created successfully', + PROJECT_PLAN_CREATED: 'Project Plan created successfully', CATEGORY_ALREADY_EXISTS: 'Category already exists', REQUIRED_FIELDS_NOT_PRESENT_FOR_THE_TASK_UPDATE: 'Required minimum fields _id or name are not present for the task creation.', diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 75e9f6a9..b8f7ea98 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -4435,6 +4435,389 @@ module.exports = class UserProjectsHelper { } catch (err) {} } + /** + * Create project plan from multiple templates. + * @method + * @name createProjectPlan + * @param {Object} data - request data. + * @param {String} userId - Logged in user id (admin). + * @param {String} userToken - User token. + * @param {Object} userDetails - loggedin user's info + * @returns {Object} Project Plan created information. + */ + static createProjectPlan(data, userId, userToken, userDetails) { + return new Promise(async (resolve, reject) => { + try { + const { templates, userId: participantId, entityId, programName, projectConfig } = data + let tenantId = userDetails.userInformation.tenantId + let orgId = userDetails.userInformation.organizationId + + // Step 1: Validations + // a. Validate Participant User + let participantProfile = await userService.profile(participantId, userToken) + if (!participantProfile.success || !participantProfile.data) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.USER_NOT_FOUND, + } + } + + // b. Validate Entity + if (entityId) { + let entityInformation = await entitiesService.entityDocuments( + { _id: entityId, tenantId: tenantId }, + CONSTANTS.common.ALL + ) + if (!entityInformation?.success || !entityInformation?.data?.length > 0) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.ENTITY_NOT_FOUND, + } + } + } + + // c. Validate all Template IDs - must exist and be published + // Filter out any templates without templateId + const templateIds = templates + .map((t) => (t && t.templateId ? t.templateId : null)) + .filter((id) => id !== null) + + if (templateIds.length === 0) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: 'No valid template IDs provided', + } + } + + const validTemplates = await projectTemplateQueries.templateDocument( + { + _id: { $in: templateIds }, + status: CONSTANTS.common.PUBLISHED, + tenantId: tenantId, + }, + ['_id', 'title', 'categories', 'solutionId', 'solutionExternalId', 'externalId'] + ) + + if (validTemplates.length !== templates.length) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.PROJECT_TEMPLATE_NOT_FOUND, + } + } + + // Step 2: Always Create New Program & Solution + let programAndSolutionData = { + programName: programName || `Program for ${participantProfile.data.name}`, + isPrivateProgram: true, + type: CONSTANTS.common.IMPROVEMENT_PROJECT, + subType: CONSTANTS.common.IMPROVEMENT_PROJECT, + isReusable: false, + entities: entityId ? [entityId] : [], + } + + let programAndSolutionInformation = await solutionsHelper.createProgramAndSolution( + userId, + programAndSolutionData, + false, + userDetails, + true // isExternalProgram + ) + + if (!programAndSolutionInformation.success) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.SOLUTION_PROGRAMS_NOT_CREATED, + } + } + + const masterProgramId = programAndSolutionInformation.result.program._id + const masterSolutionId = programAndSolutionInformation.result.solution._id + + // Step 3: Initialize Single Project + let projectData = { + title: projectConfig?.name || `${participantProfile.data.name}'s Project Plan`, + description: projectConfig?.description || 'Individual Development Plan', + userId: participantId, + createdBy: userId, + updatedBy: userId, + status: CONSTANTS.common.STARTED, + lastDownloadedAt: new Date(), + isAPrivateProgram: true, + programId: masterProgramId, + programExternalId: programAndSolutionInformation.result.program.externalId, + solutionId: masterSolutionId, + solutionExternalId: programAndSolutionInformation.result.solution.externalId, + programInformation: { + _id: masterProgramId, + name: programAndSolutionInformation.result.program.name, + externalId: programAndSolutionInformation.result.program.externalId, + }, + solutionInformation: { + _id: masterSolutionId, + name: programAndSolutionInformation.result.solution.name, + externalId: programAndSolutionInformation.result.solution.externalId, + }, + tenantId: tenantId, + orgId: orgId, + tasks: [], + taskSequence: [], + userProfile: participantProfile.data, + createdAt: new Date(), + updatedAt: new Date(), + } + + // Add entity information if provided + if (entityId) { + const entityInfo = await entitiesService.entityDocuments( + { _id: entityId, tenantId: tenantId }, + CONSTANTS.common.ALL + ) + if (entityInfo?.success && entityInfo?.data?.length > 0) { + const entity = entityInfo.data[0] + masterProjectData.entityInformation = { + _id: entity._id, + entityType: entity.entityType, + entityTypeId: entity.entityTypeId, + entityId: entity._id, + externalId: entity?.metaInformation?.externalId || '', + entityName: entity?.metaInformation?.name || '', + } + masterProjectData.entityId = entity._id + } + } + + // Step 4: Template Orchestration (Loop for each template) + for (let templateIndex = 0; templateIndex < templates.length; templateIndex++) { + const template = templates[templateIndex] + + // Validate template has templateId + if (!template || !template.templateId) { + continue + } + + // a. Fetch Template metadata + const templateData = validTemplates.find((t) => { + if (!t || !t._id || !template.templateId) return false + return t._id.toString() === template.templateId.toString() + }) + + if (!templateData) { + continue // Skip invalid templates + } + + // b. Create improvementProject task at root level first (to get its ID) + const taskName = + template.targetTaskName || + template.targetProjectName || + templateData.title || + `Template ${templateIndex + 1}` + const taskExternalId = `task-${uuidv4().replace(/-/g, '')}` + const improvementTaskId = uuidv4() + + // c. Fetch Template Tasks and Subtasks + const templateTasks = await projectTemplatesHelper.tasksAndSubTasks( + template.templateId, + '', // language + tenantId, + orgId + ) + + if (!templateTasks || templateTasks.length === 0) { + continue + } + + // Ensure all tasks have _id before processing (required by _projectTask) + const tasksWithIds = templateTasks + .map((task) => { + if (task && !task._id) { + task._id = uuidv4() + } + return task + }) + .filter((task) => task !== null && task !== undefined) + + // d. Process Template Tasks using _projectTask with improvementTaskId as parent + let processedTemplateTasks = [] + try { + processedTemplateTasks = await _projectTask( + tasksWithIds, + true, // isImportedFromLibrary + improvementTaskId, // parentTaskId - set to improvementTask._id + userToken, + masterProgramId, + userDetails + ) + } catch (error) { + console.error(`Error processing template tasks for template ${template.templateId}:`, error) + throw error + } + + // Ensure processedTemplateTasks is an array + if (!Array.isArray(processedTemplateTasks)) { + processedTemplateTasks = [] + } + + // e. Process Custom Tasks if provided + let processedCustomTasks = [] + if (template.customTasks && template.customTasks.length > 0) { + // Ensure all custom tasks have _id before processing + const customTasksWithIds = template.customTasks + .map((task) => { + if (task && !task._id) { + task._id = uuidv4() + } + return task + }) + .filter((task) => task !== null && task !== undefined) + + try { + processedCustomTasks = await _projectTask( + customTasksWithIds, + false, // isImportedFromLibrary + improvementTaskId, // parentTaskId - set to improvementTask._id + userToken, + masterProgramId, + userDetails + ) + } catch (error) { + console.error(`Error processing custom tasks for template ${template.templateId}:`, error) + throw error + } + + // Ensure processedCustomTasks is an array + if (!Array.isArray(processedCustomTasks)) { + processedCustomTasks = [] + } + + // Mark all custom tasks as isACustomTask: true + processedCustomTasks.forEach((customTask) => { + customTask.isACustomTask = true + customTask.createdBy = userId + customTask.updatedBy = userId + customTask.createdAt = new Date() + customTask.updatedAt = new Date() + }) + } + + // f. Ensure parentId is set correctly for all root-level subtasks + if (processedTemplateTasks && Array.isArray(processedTemplateTasks)) { + processedTemplateTasks.forEach((task) => { + if (task && (!task.parentId || task.parentId !== improvementTaskId)) { + task.parentId = improvementTaskId + } + }) + } + if (processedCustomTasks && Array.isArray(processedCustomTasks)) { + processedCustomTasks.forEach((task) => { + if (task && (!task.parentId || task.parentId !== improvementTaskId)) { + task.parentId = improvementTaskId + } + }) + } + + // g. Combine template tasks and custom tasks as children + const allSubTasks = [...processedTemplateTasks, ...processedCustomTasks] + + let improvementTask = { + _id: improvementTaskId, + externalId: taskExternalId, + name: taskName, + description: template.targetTaskName || template.targetProjectName || templateData.title || '', + type: CONSTANTS.common.IMPROVEMENT_PROJECT, + status: CONSTANTS.common.NOT_STARTED_STATUS, + isACustomTask: false, + isDeletable: false, + isDeleted: false, + isImportedFromLibrary: false, + createdAt: new Date(), + updatedAt: new Date(), + createdBy: userId, + updatedBy: userId, + children: allSubTasks, // Template tasks + custom tasks as subtasks + attachments: [], + projectTemplateDetails: { + _id: template.templateId, + externalId: templateData && templateData.externalId ? templateData.externalId : '', + name: templateData && templateData.title ? templateData.title : taskName, + }, + } + + // Add improvementProject task to project + projectData.tasks.push(improvementTask) + projectData.taskSequence.push(taskExternalId) + + // Add subtask externalIds to taskSequence (after improvementProject task) + if (allSubTasks && Array.isArray(allSubTasks)) { + allSubTasks.forEach((subTask) => { + if ( + subTask && + subTask.externalId && + !projectData.taskSequence.includes(subTask.externalId) + ) { + projectData.taskSequence.push(subTask.externalId) + } + }) + } + } + + // Step 5: Initialize task report for Project + // Count all tasks (root level improvementProject tasks) + const activeTasks = projectData.tasks.filter((t) => !t.isDeleted) + let taskReport = { + total: activeTasks.length, + } + + activeTasks.forEach((task) => { + if (task.isDeleted == false) { + if (!taskReport[task.status]) { + taskReport[task.status] = 1 + } else { + taskReport[task.status] += 1 + } + } + }) + + projectData.taskReport = taskReport + // Step 6: Create Single Project + // Ensure tasks array is properly initialized (should already be an array) + if (!Array.isArray(projectData.tasks)) { + projectData.tasks = [] + } + + let createdProject = await projectQueries.createProject(projectData) + // Verify tasks were saved + if (!createdProject.tasks || createdProject.tasks.length === 0) { + console.error('WARNING: Project created but tasks array is empty!') + console.error('Tasks that should have been saved:', projectData.tasks.length) + } + + // Push to Kafka for event streaming + await this.attachEntityInformationIfExists(createdProject) + await kafkaProducersHelper.pushProjectToKafka(createdProject) + await kafkaProducersHelper.pushUserActivitiesToKafka({ + userId: participantId, + projects: createdProject, + }) + + return resolve({ + success: true, + message: CONSTANTS.apiResponses.PROJECT_PLAN_CREATED, + data: { + projectId: createdProject._id, + }, + result: { + projectId: createdProject._id, + }, + }) + } catch (error) { + return reject({ + status: error.status ? error.status : HTTP_STATUS_CODE.internal_server_error.status, + message: error.message || error, + }) + } + }) + } + /** * Get project infromation when project as a task * @method diff --git a/module/userProjects/validator/v1.js b/module/userProjects/validator/v1.js index 780ffb75..47001ce2 100644 --- a/module/userProjects/validator/v1.js +++ b/module/userProjects/validator/v1.js @@ -132,6 +132,62 @@ module.exports = (req) => { } req.checkQuery(existId).exists().withMessage('required solution or projectId Id') }, + createProjectPlan: function () { + // Validate templates array + req.checkBody('templates') + .exists() + .withMessage('templates array is required') + .isArray() + .withMessage('templates must be an array') + .custom((templates) => { + if (!templates || templates.length === 0) { + throw new Error('templates array cannot be empty') + } + return templates.every((template) => { + if (!template.templateId) { + throw new Error('Each template must have a templateId') + } + return true + }) + }) + + // Validate userId - must be provided and not empty + req.checkBody('userId') + .exists() + .withMessage('userId is required') + .notEmpty() + .withMessage('userId cannot be empty') + + // Validate entityId - must be provided and not empty + req.checkBody('entityId') + .exists() + .withMessage('entityId is required') + .notEmpty() + .withMessage('entityId cannot be empty') + + // Validate projectConfig + req.checkBody('projectConfig') + .exists() + .withMessage('projectConfig is required') + .custom((config) => { + if (!config || typeof config !== 'object') { + throw new Error('projectConfig must be an object') + } + if (!config.name) { + throw new Error('projectConfig.name is required') + } + return true + }) + + // Validate isPrivateProgram - must always be true + req.checkBody('isPrivateProgram') + .exists() + .withMessage('isPrivateProgram is required') + .isBoolean() + .withMessage('isPrivateProgram must be a boolean') + .equals(true) + .withMessage('isPrivateProgram must always be true') + }, } if (projectsValidator[req.params.method]) { From f381ced39a2ebaf997939209c8d4675b5a34f67d Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:49:03 +0530 Subject: [PATCH 20/44] Issue#252622 Feat: Importing project from multiple templates --- controllers/v1/userProjects.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/controllers/v1/userProjects.js b/controllers/v1/userProjects.js index b99d7cd1..6b974409 100644 --- a/controllers/v1/userProjects.js +++ b/controllers/v1/userProjects.js @@ -1588,13 +1588,6 @@ module.exports = class UserProjects extends Abstract { async createProjectPlan(req) { return new Promise(async (resolve, reject) => { try { - // Check if userDetails is properly set by authentication middleware - if (!req.userDetails || !req.userDetails.userInformation) { - return resolve({ - status: HTTP_STATUS_CODE.unauthorized.status, - message: 'Authentication failed - user details not available', - }) - } let result = await userProjectsHelper.createProjectPlan( req.body, req.userDetails.userInformation.userId, From e4489a17a22d6c45504f93b724b94f6dcdec4b2b Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:35:35 +0530 Subject: [PATCH 21/44] Issue#252622 Feat: Importing project from multiple templates --- module/userProjects/helper.js | 89 +++++++++++++++++++++++------ module/userProjects/validator/v1.js | 5 +- 2 files changed, 77 insertions(+), 17 deletions(-) diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index b8f7ea98..7afce90d 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -4495,9 +4495,36 @@ module.exports = class UserProjectsHelper { status: CONSTANTS.common.PUBLISHED, tenantId: tenantId, }, - ['_id', 'title', 'categories', 'solutionId', 'solutionExternalId', 'externalId'] + ['_id', 'title', 'categories', 'solutionId', 'solutionExternalId', 'externalId', 'taskSequence'] ) + // Collect all unique category IDs from templates + let allCategoryIds = [] + const categoryIdSet = new Set() // To avoid duplicates + validTemplates.forEach((template) => { + if (template.categories && Array.isArray(template.categories)) { + template.categories.forEach((category) => { + const categoryId = category._id?.toString() || category + if (categoryId && !categoryIdSet.has(categoryId)) { + categoryIdSet.add(categoryId) + allCategoryIds.push(categoryId) + } + }) + } + }) + + // Fetch full category documents from projectCategories collection + let allCategories = [] + if (allCategoryIds.length > 0) { + allCategories = await projectCategoriesQueries.categoryDocuments( + { + _id: { $in: allCategoryIds }, + tenantId: tenantId, + }, + ['_id', 'name', 'externalId', 'evidences'] + ) + } + if (validTemplates.length !== templates.length) { throw { status: HTTP_STATUS_CODE.bad_request.status, @@ -4559,6 +4586,7 @@ module.exports = class UserProjectsHelper { }, tenantId: tenantId, orgId: orgId, + categories: allCategories, // Add categories from all templates tasks: [], taskSequence: [], userProfile: participantProfile.data, @@ -4574,7 +4602,7 @@ module.exports = class UserProjectsHelper { ) if (entityInfo?.success && entityInfo?.data?.length > 0) { const entity = entityInfo.data[0] - masterProjectData.entityInformation = { + projectData.entityInformation = { _id: entity._id, entityType: entity.entityType, entityTypeId: entity.entityTypeId, @@ -4582,7 +4610,7 @@ module.exports = class UserProjectsHelper { externalId: entity?.metaInformation?.externalId || '', entityName: entity?.metaInformation?.name || '', } - masterProjectData.entityId = entity._id + projectData.entityId = entity._id } } @@ -4718,6 +4746,43 @@ module.exports = class UserProjectsHelper { // g. Combine template tasks and custom tasks as children const allSubTasks = [...processedTemplateTasks, ...processedCustomTasks] + // h. Build taskSequence for improvementTask based on template's taskSequence + let improvementTaskSequence = [] + + // If template has taskSequence, use it to order the subtasks + if (templateData.taskSequence && templateData.taskSequence.length > 0) { + // Create a map of externalId to task for quick lookup + const taskMap = new Map() + allSubTasks.forEach((task) => { + if (task && task.externalId) { + taskMap.set(task.externalId, task) + } + }) + + // First, add tasks in template's taskSequence order + templateData.taskSequence.forEach((templateTaskExternalId) => { + const task = taskMap.get(templateTaskExternalId) + if (task && task.externalId) { + improvementTaskSequence.push(task.externalId) + taskMap.delete(templateTaskExternalId) // Remove to avoid duplicates + } + }) + + // Then, add any remaining tasks (custom tasks or tasks not in template sequence) + taskMap.forEach((task) => { + if (task && task.externalId) { + improvementTaskSequence.push(task.externalId) + } + }) + } else { + // If no template taskSequence, use the order of processed tasks + allSubTasks.forEach((subTask) => { + if (subTask && subTask.externalId) { + improvementTaskSequence.push(subTask.externalId) + } + }) + } + let improvementTask = { _id: improvementTaskId, externalId: taskExternalId, @@ -4733,7 +4798,11 @@ module.exports = class UserProjectsHelper { updatedAt: new Date(), createdBy: userId, updatedBy: userId, + tenantId: tenantId, + orgId: orgId, + syncedAt: new Date(), children: allSubTasks, // Template tasks + custom tasks as subtasks + taskSequence: improvementTaskSequence, // Children's externalIds in correct order attachments: [], projectTemplateDetails: { _id: template.templateId, @@ -4743,21 +4812,9 @@ module.exports = class UserProjectsHelper { } // Add improvementProject task to project + // Root taskSequence should only contain improvementTask externalIds (not subtasks) projectData.tasks.push(improvementTask) projectData.taskSequence.push(taskExternalId) - - // Add subtask externalIds to taskSequence (after improvementProject task) - if (allSubTasks && Array.isArray(allSubTasks)) { - allSubTasks.forEach((subTask) => { - if ( - subTask && - subTask.externalId && - !projectData.taskSequence.includes(subTask.externalId) - ) { - projectData.taskSequence.push(subTask.externalId) - } - }) - } } // Step 5: Initialize task report for Project diff --git a/module/userProjects/validator/v1.js b/module/userProjects/validator/v1.js index 47001ce2..cccda44d 100644 --- a/module/userProjects/validator/v1.js +++ b/module/userProjects/validator/v1.js @@ -185,7 +185,10 @@ module.exports = (req) => { .withMessage('isPrivateProgram is required') .isBoolean() .withMessage('isPrivateProgram must be a boolean') - .equals(true) + .custom((value) => { + // Explicitly check for boolean true or string "true" + return value === true || value === 'true' + }) .withMessage('isPrivateProgram must always be true') }, } From 91ec8725698695cab240c6fb09a141e8c53205ba Mon Sep 17 00:00:00 2001 From: vaishali k Date: Tue, 20 Jan 2026 15:44:08 +0530 Subject: [PATCH 22/44] Routes added --- module/programUsers/helper.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/module/programUsers/helper.js b/module/programUsers/helper.js index 740d741e..8dd16297 100644 --- a/module/programUsers/helper.js +++ b/module/programUsers/helper.js @@ -166,6 +166,7 @@ module.exports = class ProgramUsersHelper { const entities = result.result.entities || [] // Count entities by status const statusCounts = { + notonboarded: 0, onboarded: 0, inprogress: 0, completed: 0, @@ -179,6 +180,9 @@ module.exports = class ProgramUsersHelper { case 'ONBOARDED': statusCounts.onboarded++ break + case 'NOT_ONBOARDED': + statusCounts.notonboarded++ + break case 'IN_PROGRESS': statusCounts.inprogress++ break @@ -199,6 +203,7 @@ module.exports = class ProgramUsersHelper { .updateOverview(result.result._id, { $set: { 'overview.assigned': statusCounts.total, + 'overview.notonboarded': statusCounts.notonboarded, 'overview.onboarded': statusCounts.onboarded, 'overview.inprogress': statusCounts.inprogress, 'overview.completed': statusCounts.completed, From 07f7e3550febee2686c3ba477d996d8ca719e7be Mon Sep 17 00:00:00 2001 From: vaishali k Date: Tue, 20 Jan 2026 19:08:18 +0530 Subject: [PATCH 23/44] filter by search and status --- controllers/v1/programUsers.js | 3 ++- generics/services/programUsers.js | 34 +++++++++++++++++++++++++++---- module/programUsers/helper.js | 2 ++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/controllers/v1/programUsers.js b/controllers/v1/programUsers.js index 36128b4b..7efe7839 100644 --- a/controllers/v1/programUsers.js +++ b/controllers/v1/programUsers.js @@ -53,7 +53,7 @@ module.exports = class ProgramUsers extends Abstract { async getEntities(req) { return new Promise(async (resolve, reject) => { try { - const { userId, programId, programExternalId, page = 1, limit = 20, search = '' } = req.query + const { userId, programId, programExternalId, page = 1, limit = 20, status, search = '' } = req.query if (!userId || (!programId && !programExternalId)) { return reject({ @@ -68,6 +68,7 @@ module.exports = class ProgramUsers extends Abstract { programExternalId, parseInt(page), parseInt(limit), + status, search, req.userDetails ) diff --git a/generics/services/programUsers.js b/generics/services/programUsers.js index 9d4e133f..98b9e842 100644 --- a/generics/services/programUsers.js +++ b/generics/services/programUsers.js @@ -151,6 +151,7 @@ module.exports = class ProgramUsersService { programExternalId, page = 1, limit = 20, + status, searchQuery = '', userDetails ) { @@ -170,15 +171,30 @@ module.exports = class ProgramUsersService { // Get entities from the found document let filteredEntities = docData.entities || [] + + // Filter by status if provided + if (status) { + filteredEntities = filteredEntities.filter((entity) => entity.status == status) + } + + // Filter by search query if provided + if (searchQuery) { + const lowerSearch = searchQuery.toLowerCase() + filteredEntities = filteredEntities.filter((entity) => { + // Assuming entity has a 'name' field to search against + return entity.name && entity.name.toLowerCase().includes(lowerSearch) + }) + } + // Apply pagination const totalCount = filteredEntities.length const paginatedEntities = filteredEntities.slice(skip, skip + limit) const userIds = paginatedEntities.map((entity) => entity.userId).filter(Boolean) - // if (process.env.PROGRAM_USERS_ENTITIES === 'users') { // Fetch user details from user service const { success, data } = (await userService.accountSearch(userIds, userDetails.userInformation.tenantId)) || {} + // Throw error if no valid users returned from service if (!success || !data || data.count === 0) { throw { @@ -187,12 +203,22 @@ module.exports = class ProgramUsersService { message: 'No valid users found for the provided entity user IDs.', } } + + // Map accountSearch data with entity data from docData and filter by searchQuery + const filteredData = paginatedEntities.map((entity) => { + const userData = data.data.find((user) => user.id == entity.userId) + return { + ...entity, + userDetails: userData || null, + } + }) + return { status: 200, message: 'Entities retrieved successfully', - data: { data: data.data, overview: docData.overview || {} }, - result: { data: data.data, overview: docData.overview || {} }, - count: data.count, + data: { data: filteredData, overview: docData.overview || {} }, + result: { data: filteredData, overview: docData.overview || {} }, + count: filteredData.length, total: totalCount, } } catch (error) { diff --git a/module/programUsers/helper.js b/module/programUsers/helper.js index 8dd16297..0922ac51 100644 --- a/module/programUsers/helper.js +++ b/module/programUsers/helper.js @@ -253,6 +253,7 @@ module.exports = class ProgramUsersHelper { programExternalId, page = 1, limit = 20, + status, search = '', userDetails ) { @@ -264,6 +265,7 @@ module.exports = class ProgramUsersHelper { programExternalId, page, limit, + status, search, userDetails ) From 31c6edd58ee2ff9910bdcd07ba615d57283112ac Mon Sep 17 00:00:00 2001 From: vaishali k Date: Wed, 21 Jan 2026 12:01:53 +0530 Subject: [PATCH 24/44] Issue #000 fix: limit and page not working --- controllers/v1/programUsers.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/controllers/v1/programUsers.js b/controllers/v1/programUsers.js index 7efe7839..d67f96fb 100644 --- a/controllers/v1/programUsers.js +++ b/controllers/v1/programUsers.js @@ -53,7 +53,8 @@ module.exports = class ProgramUsers extends Abstract { async getEntities(req) { return new Promise(async (resolve, reject) => { try { - const { userId, programId, programExternalId, page = 1, limit = 20, status, search = '' } = req.query + const { userId, programId, programExternalId, status, search = '' } = req.query + const { pageNo = 1, pageSize = 20 } = req if (!userId || (!programId && !programExternalId)) { return reject({ @@ -66,8 +67,8 @@ module.exports = class ProgramUsers extends Abstract { userId, programId, programExternalId, - parseInt(page), - parseInt(limit), + parseInt(pageNo), + parseInt(pageSize), status, search, req.userDetails From 73cada4e35da77cf8b6301b96f1dd252280b0dc6 Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:08:33 +0530 Subject: [PATCH 25/44] Issue#252622 Feat: Importing project from multiple templates --- .../sunbird-project/configs.json | 14 ---------- module/userProjects/helper.js | 10 +++---- module/userProjects/validator/v1.js | 26 ------------------- 3 files changed, 5 insertions(+), 45 deletions(-) diff --git a/constants/interface-routes/sunbird-project/configs.json b/constants/interface-routes/sunbird-project/configs.json index 4f331724..50f55856 100644 --- a/constants/interface-routes/sunbird-project/configs.json +++ b/constants/interface-routes/sunbird-project/configs.json @@ -1406,20 +1406,6 @@ ], "service": "project" }, - { - "sourceRoute": "/project/v1/userProjects/createProjectPlan", - "type": "POST", - "priority": "MUST_HAVE", - "inSequence": false, - "orchestrated": false, - "targetPackages": [ - { - "basePackageName": "project", - "packageName": "sunbird-project" - } - ], - "service": "project" - }, { "sourceRoute": "/project/v1/userProjects/update", "type": "POST", diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 7afce90d..198528e1 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -4454,8 +4454,8 @@ module.exports = class UserProjectsHelper { // Step 1: Validations // a. Validate Participant User - let participantProfile = await userService.profile(participantId, userToken) - if (!participantProfile.success || !participantProfile.data) { + let userProfile = await userService.profile(participantId, userToken) + if (!userProfile.success || !userProfile.data) { throw { status: HTTP_STATUS_CODE.bad_request.status, message: CONSTANTS.apiResponses.USER_NOT_FOUND, @@ -4534,7 +4534,7 @@ module.exports = class UserProjectsHelper { // Step 2: Always Create New Program & Solution let programAndSolutionData = { - programName: programName || `Program for ${participantProfile.data.name}`, + programName: programName || `Program for ${userProfile.data.name}`, isPrivateProgram: true, type: CONSTANTS.common.IMPROVEMENT_PROJECT, subType: CONSTANTS.common.IMPROVEMENT_PROJECT, @@ -4562,7 +4562,7 @@ module.exports = class UserProjectsHelper { // Step 3: Initialize Single Project let projectData = { - title: projectConfig?.name || `${participantProfile.data.name}'s Project Plan`, + title: projectConfig?.name || `${userProfile.data.name}'s Project Plan`, description: projectConfig?.description || 'Individual Development Plan', userId: participantId, createdBy: userId, @@ -4589,7 +4589,7 @@ module.exports = class UserProjectsHelper { categories: allCategories, // Add categories from all templates tasks: [], taskSequence: [], - userProfile: participantProfile.data, + userProfile: userProfile.data, createdAt: new Date(), updatedAt: new Date(), } diff --git a/module/userProjects/validator/v1.js b/module/userProjects/validator/v1.js index cccda44d..5869baa6 100644 --- a/module/userProjects/validator/v1.js +++ b/module/userProjects/validator/v1.js @@ -164,32 +164,6 @@ module.exports = (req) => { .withMessage('entityId is required') .notEmpty() .withMessage('entityId cannot be empty') - - // Validate projectConfig - req.checkBody('projectConfig') - .exists() - .withMessage('projectConfig is required') - .custom((config) => { - if (!config || typeof config !== 'object') { - throw new Error('projectConfig must be an object') - } - if (!config.name) { - throw new Error('projectConfig.name is required') - } - return true - }) - - // Validate isPrivateProgram - must always be true - req.checkBody('isPrivateProgram') - .exists() - .withMessage('isPrivateProgram is required') - .isBoolean() - .withMessage('isPrivateProgram must be a boolean') - .custom((value) => { - // Explicitly check for boolean true or string "true" - return value === true || value === 'true' - }) - .withMessage('isPrivateProgram must always be true') }, } From 79d5a80cd0240611ae1c3ded4916a2ebc8a8e8b6 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Wed, 21 Jan 2026 15:59:40 +0530 Subject: [PATCH 26/44] Issue #000 fix: getEntities by entityId --- controllers/v1/programUsers.js | 3 ++- generics/services/programUsers.js | 16 ++++++++++++++++ module/programUsers/helper.js | 2 ++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/controllers/v1/programUsers.js b/controllers/v1/programUsers.js index d67f96fb..39edb26f 100644 --- a/controllers/v1/programUsers.js +++ b/controllers/v1/programUsers.js @@ -53,7 +53,7 @@ module.exports = class ProgramUsers extends Abstract { async getEntities(req) { return new Promise(async (resolve, reject) => { try { - const { userId, programId, programExternalId, status, search = '' } = req.query + const { userId, programId, programExternalId, status, search = '', entityId = '' } = req.query const { pageNo = 1, pageSize = 20 } = req if (!userId || (!programId && !programExternalId)) { @@ -71,6 +71,7 @@ module.exports = class ProgramUsers extends Abstract { parseInt(pageSize), status, search, + entityId, req.userDetails ) return resolve(result) diff --git a/generics/services/programUsers.js b/generics/services/programUsers.js index 98b9e842..d921b671 100644 --- a/generics/services/programUsers.js +++ b/generics/services/programUsers.js @@ -143,6 +143,7 @@ module.exports = class ProgramUsersService { * @param {Number} page - page number * @param {Number} limit - items per page * @param {String} searchQuery - search text + * @param {String} entityId - specific entity ID to fetch (optional) * @returns {Object} entities with pagination info */ static async getEntitiesWithPagination( @@ -153,6 +154,7 @@ module.exports = class ProgramUsersService { limit = 20, status, searchQuery = '', + entityId, userDetails ) { try { @@ -172,6 +174,20 @@ module.exports = class ProgramUsersService { // Get entities from the found document let filteredEntities = docData.entities || [] + // Filter by specific entityId if provided + if (entityId) { + filteredEntities = filteredEntities.filter( + (entity) => entity.userId === entityId || entity.entityId === entityId + ) + if (filteredEntities.length === 0) { + return { + status: 404, + message: 'Entity not found', + data: [], + } + } + } + // Filter by status if provided if (status) { filteredEntities = filteredEntities.filter((entity) => entity.status == status) diff --git a/module/programUsers/helper.js b/module/programUsers/helper.js index 0922ac51..85e2ebf3 100644 --- a/module/programUsers/helper.js +++ b/module/programUsers/helper.js @@ -255,6 +255,7 @@ module.exports = class ProgramUsersHelper { limit = 20, status, search = '', + entityId, userDetails ) { try { @@ -267,6 +268,7 @@ module.exports = class ProgramUsersHelper { limit, status, search, + entityId, userDetails ) From 60906ab699f81a9973e0d78953a96777583fd728 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Wed, 21 Jan 2026 16:33:37 +0530 Subject: [PATCH 27/44] Feat: API to update the specific entity of the LC entities --- controllers/v1/programUsers.js | 8 ++++ databaseQueries/programUsers.js | 59 +++++++++++++++++++++++ generics/services/programUsers.js | 28 +++++++++++ module/programUsers/helper.js | 77 +++++++++++++++++++++++++++++++ module/userProjects/helper.js | 5 +- 5 files changed, 176 insertions(+), 1 deletion(-) diff --git a/controllers/v1/programUsers.js b/controllers/v1/programUsers.js index 39edb26f..eed9fd7f 100644 --- a/controllers/v1/programUsers.js +++ b/controllers/v1/programUsers.js @@ -23,6 +23,7 @@ module.exports = class ProgramUsers extends Abstract { /** * Create or update program user * Supports: create, update, add entity, update status + * Routes to updateEntity if entityId and entityUpdates are provided * @method * @name createOrUpdate * @param {Object} req - request object @@ -31,6 +32,13 @@ module.exports = class ProgramUsers extends Abstract { async createOrUpdate(req) { return new Promise(async (resolve, reject) => { try { + // Check if this is an entity-specific update + if (req.body.entityId && req.body.entityUpdates) { + const result = await programUsersHelper.updateEntity(req.body, req.userDetails) + return resolve(result) + } + + // Regular createOrUpdate flow const result = await programUsersHelper.createOrUpdate(req.body, req.userDetails) return resolve(result) diff --git a/databaseQueries/programUsers.js b/databaseQueries/programUsers.js index 51cc1365..36db2953 100644 --- a/databaseQueries/programUsers.js +++ b/databaseQueries/programUsers.js @@ -327,4 +327,63 @@ module.exports = class programUsers { } }) } + + /** + * Update specific fields of an entity within a program user + * @method + * @name updateEntity + * @param {String} userId - program user's userId + * @param {String} programId - program ID (optional) + * @param {String} programExternalId - program external ID (optional) + * @param {String} entityId - entity's userId to identify which entity to update + * @param {Object|Array} entityUpdates - fields to update on the entity (object or array of objects) + * @param {String} tenantId - tenant ID + * @returns {Object} updated program user document + */ + static updateEntity(userId, programId, programExternalId, entityId, entityUpdates, tenantId) { + return new Promise(async (resolve, reject) => { + try { + // Build query: use programId if available, otherwise programExternalId + const query = { userId, tenantId } + if (programId) { + query.programId = programId + } else if (programExternalId) { + query.programExternalId = programExternalId + } + + // Only support regular objects like {status: "ONBOARDED", myList: [{1:1}]} + // Handle edge case: if array was converted to object with numeric keys (e.g., {0: {...}}) + let updates = entityUpdates + + if (typeof entityUpdates !== 'object' || entityUpdates === null) { + return reject({ + message: 'entityUpdates must be an object', + status: 400, + }) + } + + // Build the $set operations with dot notation for nested fields + const setOperations = { updatedAt: new Date() } + Object.keys(updates).forEach((key) => { + // Only process non-numeric keys (regular object properties) + if (!/^\d+$/.test(key)) { + setOperations[`entities.$.${key}`] = updates[key] + } + }) + + // Find and update: match the entity by userId or externalId + let result = await database.models.programUsers.findOneAndUpdate( + { ...query, $or: [{ 'entities.userId': entityId }, { 'entities.externalId': entityId }] }, + { + $set: setOperations, + }, + { new: true, lean: true } + ) + + return resolve(result) + } catch (error) { + return reject(error) + } + }) + } } diff --git a/generics/services/programUsers.js b/generics/services/programUsers.js index d921b671..b6c7b892 100644 --- a/generics/services/programUsers.js +++ b/generics/services/programUsers.js @@ -259,4 +259,32 @@ module.exports = class ProgramUsersService { throw error } } + + /** + * Update specific fields of an entity within a program user + * @method + * @name updateEntity + * @param {String} userId - program user's userId + * @param {String} programId - program ID (optional) + * @param {String} programExternalId - program external ID (optional) + * @param {String} entityId - entity's userId to identify which entity to update + * @param {Object} entityUpdates - fields to update on the entity + * @param {String} tenantId - tenant ID + * @returns {Object} updated program user document + */ + static async updateEntity(userId, programId, programExternalId, entityId, entityUpdates, tenantId) { + try { + const result = await programUsersQueries.updateEntity( + userId, + programId, + programExternalId, + entityId, + entityUpdates, + tenantId + ) + return result + } catch (error) { + throw error + } + } } diff --git a/module/programUsers/helper.js b/module/programUsers/helper.js index 85e2ebf3..4f528b01 100644 --- a/module/programUsers/helper.js +++ b/module/programUsers/helper.js @@ -399,6 +399,83 @@ module.exports = class ProgramUsersHelper { * @param {String} programExternalId - program external ID * @returns {Boolean} true if program exists, false otherwise */ + /** + * Update specific entity fields within a program user + * @method + * @name updateEntity + * @param {Object} data - request body data + * @param {String} data.userId - program user's userId + * @param {String} data.programId - program ID (optional) + * @param {String} data.programExternalId - program external ID (optional) + * @param {String} data.entityId - entity's userId to identify which entity to update + * @param {Object} data.entityUpdates - fields to update on the entity + * @param {Object} userDetails - logged in user details + * @returns {Object} result with status and updated entity data + */ + static async updateEntity(data, userDetails) { + try { + const { userId, programId, programExternalId, entityId, entityUpdates } = data + const tenantId = userDetails.userInformation?.tenantId + + // Validate required fields + if (!userId || (!programId && !programExternalId)) { + return { + success: false, + status: HTTP_STATUS_CODE.bad_request.status, + message: 'userId and either programId or programExternalId are required', + } + } + + if (!entityId) { + return { + success: false, + status: HTTP_STATUS_CODE.bad_request.status, + message: 'entityId is required', + } + } + + // Validate entityUpdates - only regular objects are supported (e.g., {status: "ONBOARDED", myList: [{1:1}]}) + if (!entityUpdates) { + return { + success: false, + status: HTTP_STATUS_CODE.bad_request.status, + message: 'entityUpdates is required', + } + } + + // Only accept regular objects with named properties + const isValidObject = + typeof entityUpdates === 'object' && entityUpdates !== null && Object.keys(entityUpdates).length > 0 + + if (!isValidObject) { + return { + success: false, + status: HTTP_STATUS_CODE.bad_request.status, + message: 'entityUpdates must be a non-empty object with named properties', + } + } + + // Call service to update entity + const result = await programUsersService.updateEntity( + userId, + programId, + programExternalId, + entityId, + entityUpdates, + tenantId + ) + + return { + success: true, + status: 200, + message: 'Entity updated successfully', + result, + } + } catch (error) { + throw error + } + } + static checkProgramExists(tenantId, programId, programExternalId) { return new Promise(async (resolve, reject) => { try { diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index c99fd70a..475beb37 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -3094,7 +3094,10 @@ module.exports = class UserProjectsHelper { ) if (!entityInformation?.success || !entityInformation?.data?.length > 0) { - return resolve(entityInformation) + return resolve({ + success: false, + message: 'Entity information not found for the given entityId', + }) } libraryProjects.data['entityInformation'] = entityInformation.data[0] From 3d6284516d37f91487749a65bb9c405528c35c77 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Wed, 21 Jan 2026 16:51:47 +0530 Subject: [PATCH 28/44] Feat: API to update the specific entity of the LC entities --- databaseQueries/programUsers.js | 50 +++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/databaseQueries/programUsers.js b/databaseQueries/programUsers.js index 36db2953..f7f89324 100644 --- a/databaseQueries/programUsers.js +++ b/databaseQueries/programUsers.js @@ -371,15 +371,59 @@ module.exports = class programUsers { } }) - // Find and update: match the entity by userId or externalId - let result = await database.models.programUsers.findOneAndUpdate( - { ...query, $or: [{ 'entities.userId': entityId }, { 'entities.externalId': entityId }] }, + // First, find the document to verify it exists and find which field matches the entity + const docData = await database.models.programUsers.findOne(query).lean() + + if (!docData) { + return reject({ + message: 'Program user not found', + status: 404, + }) + } + + // Find the entity and determine which field matches + const entity = docData.entities?.find( + (e) => e.userId === entityId || e.externalId === entityId || e.entityId === entityId + ) + + if (!entity) { + return reject({ + message: 'Entity not found in program user', + status: 404, + }) + } + + // Determine which field to use for matching (priority: userId > entityId > externalId) + let matchField = 'entities.userId' + if (entity.userId === entityId) { + matchField = 'entities.userId' + } else if (entity.entityId === entityId) { + matchField = 'entities.entityId' + } else if (entity.externalId === entityId) { + matchField = 'entities.externalId' + } + + // Build the query with the specific matching field + // The positional operator requires the array matching condition to be directly in the query + const updateQuery = { ...query } + updateQuery[matchField] = entityId + + // Perform the update + const result = await database.models.programUsers.findOneAndUpdate( + updateQuery, { $set: setOperations, }, { new: true, lean: true } ) + if (!result) { + return reject({ + message: 'Failed to update entity', + status: 500, + }) + } + return resolve(result) } catch (error) { return reject(error) From bee293d3ef0f306beb935835c489f95108e9a57b Mon Sep 17 00:00:00 2001 From: vaishali k Date: Thu, 22 Jan 2026 04:19:08 +0530 Subject: [PATCH 29/44] Issue #000 fix: updating overview count on craeteOrUpdate and updateEntity --- databaseQueries/programUsers.js | 24 ++++++ generics/services/programUsers.js | 17 ---- module/programUsers/helper.js | 129 +++++++++++++++++------------- 3 files changed, 99 insertions(+), 71 deletions(-) diff --git a/databaseQueries/programUsers.js b/databaseQueries/programUsers.js index f7f89324..62baf9ee 100644 --- a/databaseQueries/programUsers.js +++ b/databaseQueries/programUsers.js @@ -327,8 +327,32 @@ module.exports = class programUsers { } }) } + /** + * Find program user by userId and either programId or programExternalId + * @method + * @name findByUserAndProgram + * @param {String} userId - user ID + * @param {String} programId - program ID (optional) + * @param {String} programExternalId - program external ID (optional) + * @returns {Object} program user document + */ + static findById(docId) { + return new Promise(async (resolve, reject) => { + try { + // Build query: use programId if available, otherwise programExternalId + const query = { _id: docId } + + let result = await database.models.programUsers.findOne(query).lean() + + return resolve(result) + } catch (error) { + return reject(error) + } + }) + } /** + * * Update specific fields of an entity within a program user * @method * @name updateEntity diff --git a/generics/services/programUsers.js b/generics/services/programUsers.js index b6c7b892..15a73dc0 100644 --- a/generics/services/programUsers.js +++ b/generics/services/programUsers.js @@ -116,23 +116,6 @@ module.exports = class ProgramUsersService { } } - /** - * Update overview statistics - * @method - * @name updateOverview - * @param {String} docId - document _id - * @param {Object} operations - increment/decrement operations - * @returns {Object} updated document - */ - static async updateOverview(docId, operations) { - try { - const result = await programUsersQueries.updateOverview(docId, operations) - return result - } catch (error) { - throw error - } - } - /** * Get program user entities with pagination * @method diff --git a/module/programUsers/helper.js b/module/programUsers/helper.js index 4f528b01..019bc1e2 100644 --- a/module/programUsers/helper.js +++ b/module/programUsers/helper.js @@ -162,60 +162,7 @@ module.exports = class ProgramUsersHelper { // Update overview asynchronously (non-blocking) if (result.result && result.result._id) { setImmediate(async () => { - try { - const entities = result.result.entities || [] - // Count entities by status - const statusCounts = { - notonboarded: 0, - onboarded: 0, - inprogress: 0, - completed: 0, - graduated: 0, - droppedout: 0, - total: entities.length, - } - - entities.forEach((entity) => { - switch (entity.status) { - case 'ONBOARDED': - statusCounts.onboarded++ - break - case 'NOT_ONBOARDED': - statusCounts.notonboarded++ - break - case 'IN_PROGRESS': - statusCounts.inprogress++ - break - case 'COMPLETED': - statusCounts.completed++ - break - case 'GRADUATED': - statusCounts.graduated++ - break - case 'DROP_OUT': - statusCounts.droppedout++ - break - } - }) - - // Update overview with status counts - programUsersService - .updateOverview(result.result._id, { - $set: { - 'overview.assigned': statusCounts.total, - 'overview.notonboarded': statusCounts.notonboarded, - 'overview.onboarded': statusCounts.onboarded, - 'overview.inprogress': statusCounts.inprogress, - 'overview.completed': statusCounts.completed, - 'overview.graduated': statusCounts.graduated, - 'overview.droppedout': statusCounts.droppedout, - 'overview.lastModified': new Date(), - }, - }) - .catch((err) => console.error('Overview update error:', err)) - } catch (err) { - console.error('Error calculating entity counts:', err) - } + this._updateOverviewAsync(result.result._id) }) } @@ -464,6 +411,12 @@ module.exports = class ProgramUsersHelper { entityUpdates, tenantId ) + // Update overview asynchronously (non-blocking) + if (result && result._id) { + setImmediate(async () => { + this._updateOverviewAsync(result._id) + }) + } return { success: true, @@ -503,4 +456,72 @@ module.exports = class ProgramUsersHelper { } }) } + + static async _updateOverviewAsync(programUsersId) { + // Placeholder for asynchronous overview update logic + // This could involve recalculating summary statistics or other data + console.log(`[ProgramUsers] Updating overview for programUsersId: ${programUsersId}`) + + try { + // Fetch program user document + const docData = await programUsersQueries.findById(programUsersId) + if (!docData) { + console.error('Program user document not found for overview update:', programUsersId) + return + } + + const entities = docData.entities || [] + // Count entities by status + const statusCounts = { + notonboarded: 0, + onboarded: 0, + inprogress: 0, + completed: 0, + graduated: 0, + droppedout: 0, + total: entities.length, + } + + entities.forEach((entity) => { + switch (entity.status) { + case 'ONBOARDED': + statusCounts.onboarded++ + break + case 'NOT_ONBOARDED': + statusCounts.notonboarded++ + break + case 'IN_PROGRESS': + statusCounts.inprogress++ + break + case 'COMPLETED': + statusCounts.completed++ + break + case 'GRADUATED': + statusCounts.graduated++ + break + case 'DROP_OUT': + statusCounts.droppedout++ + break + } + }) + + // Update overview with status counts + const result = await programUsersQueries + .updateOverview(programUsersId, { + $set: { + 'overview.assigned': statusCounts.total, + 'overview.notonboarded': statusCounts.notonboarded, + 'overview.onboarded': statusCounts.onboarded, + 'overview.inprogress': statusCounts.inprogress, + 'overview.completed': statusCounts.completed, + 'overview.graduated': statusCounts.graduated, + 'overview.droppedout': statusCounts.droppedout, + 'overview.lastModified': new Date(), + }, + }) + .catch((err) => console.error('Overview update error:', err)) + } catch (err) { + console.error('Error calculating entity counts:', err) + } + } } From 7ae4667488711edec34af69676cf2ca861d089e5 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Thu, 22 Jan 2026 05:32:19 +0530 Subject: [PATCH 30/44] Issue #000 fix: fetching user profile from user service directly instead roting throgh project service --- module/userProjects/helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 475beb37..643faffa 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -3210,7 +3210,7 @@ module.exports = class UserProjectsHelper { //Fetch user profile information. let addReportInfoToSolution = false - let userProfile = await projectService.profileRead(userToken) + let userProfile = await userService.profile(userId, userToken) // Check if the user profile fetch was successful if (!userProfile.success) { throw { From e66f7f55d078ff9c81657849f2d93504c99503ec Mon Sep 17 00:00:00 2001 From: vaishali k Date: Thu, 22 Jan 2026 09:57:19 +0530 Subject: [PATCH 31/44] Issue #000 update user entity status when idp is assignned, and create programUsers entry for the participant --- config/kafka.js | 21 ++++++++++--- databaseQueries/programUsers.js | 55 ++++++++++++++++++++++++++++++--- generics/kafka/producers.js | 8 ++--- module/userProjects/helper.js | 16 ++++++++++ 4 files changed, 88 insertions(+), 12 deletions(-) diff --git a/config/kafka.js b/config/kafka.js index a835e2d9..3da9ff63 100644 --- a/config/kafka.js +++ b/config/kafka.js @@ -14,6 +14,7 @@ const USER_DELETE_ON_OFF = process.env.USER_DELETE_ON_OFF const COURSES_TOPIC = process.env.USER_COURSES_SUBMISSION_TOPIC const ORG_EXTENSION_TOPIC = process.env.ORG_UPDATES_TOPIC const USER_ACCOUNT_EVENT_TOPIC = process.env.USER_ACCOUNT_EVENT_TOPIC +const USER_ACTIVITY_TOPIC = process.env.USER_ACTIVITY_TOPIC /** * Kafka configurations. @@ -59,6 +60,9 @@ const connect = function () { // consume event that produced by the user service _sendToKafkaConsumers(USER_ACCOUNT_EVENT_TOPIC, process.env.KAFKA_URL) + + _sendToKafkaConsumers(USER_ACTIVITY_TOPIC, process.env.KAFKA_URL) + return { kafkaProducer: producer, kafkaClient: client, @@ -85,10 +89,10 @@ var _sendToKafkaConsumers = function (topic, host) { ) consumer.on('message', async function (message) { - console.log('-------Kafka consumer log starts here------------------') - console.log('Topic Name: ', topic) - console.log('Message: ', JSON.stringify(message)) - console.log('-------Kafka consumer log ends here------------------') + // console.log('-------Kafka consumer log starts here------------------') + // console.log('Topic Name: ', topic) + // console.log('Message: ', JSON.stringify(message)) + // console.log('-------Kafka consumer log ends here------------------') if (message && message.topic === SUBMISSION_TOPIC) { submissionsConsumer.messageReceived(message) @@ -117,6 +121,11 @@ var _sendToKafkaConsumers = function (topic, host) { if (message && message.topic === USER_ACCOUNT_EVENT_TOPIC) { userExtensionConsumer.messageReceived(message) } + + // call projectActivity consumer + if (message && message.topic === USER_ACTIVITY_TOPIC) { + userActivitiesConsumer.messageReceived(message) + } }) consumer.on('error', async function (error) { @@ -143,6 +152,10 @@ var _sendToKafkaConsumers = function (topic, host) { if (error.topics && error.topics[0] === USER_ACCOUNT_EVENT_TOPIC) { userExtensionConsumer.errorTriggered(error) } + + if (error.topics && error.topics[0] === USER_ACTIVITY_TOPIC) { + userActivitiesConsumer.errorTriggered(error) + } }) } } diff --git a/databaseQueries/programUsers.js b/databaseQueries/programUsers.js index 62baf9ee..a09a3895 100644 --- a/databaseQueries/programUsers.js +++ b/databaseQueries/programUsers.js @@ -407,7 +407,7 @@ module.exports = class programUsers { // Find the entity and determine which field matches const entity = docData.entities?.find( - (e) => e.userId === entityId || e.externalId === entityId || e.entityId === entityId + (e) => e.userId == entityId || e.externalId == entityId || e.entityId == entityId ) if (!entity) { @@ -419,11 +419,11 @@ module.exports = class programUsers { // Determine which field to use for matching (priority: userId > entityId > externalId) let matchField = 'entities.userId' - if (entity.userId === entityId) { + if (entity.userId == entityId) { matchField = 'entities.userId' - } else if (entity.entityId === entityId) { + } else if (entity.entityId == entityId) { matchField = 'entities.entityId' - } else if (entity.externalId === entityId) { + } else if (entity.externalId == entityId) { matchField = 'entities.externalId' } @@ -432,6 +432,19 @@ module.exports = class programUsers { const updateQuery = { ...query } updateQuery[matchField] = entityId + // Log for debugging + console.log('updateEntity - Debug Info:', { + userId, + programId, + programExternalId, + entityId, + matchField, + updateQuery: JSON.stringify(updateQuery), + setOperations: JSON.stringify(setOperations), + entityFound: entity ? true : false, + entityData: entity ? JSON.stringify(entity) : null, + }) + // Perform the update const result = await database.models.programUsers.findOneAndUpdate( updateQuery, @@ -442,14 +455,48 @@ module.exports = class programUsers { ) if (!result) { + // Enhanced error logging + console.error('updateEntity - Update failed:', { + message: 'findOneAndUpdate returned null', + updateQuery: JSON.stringify(updateQuery), + setOperations: JSON.stringify(setOperations), + userId, + programId, + programExternalId, + entityId, + matchField, + originalQuery: JSON.stringify(query), + docDataExists: docData ? true : false, + entityExists: entity ? true : false, + }) + + // Check if document still exists + const docStillExists = await database.models.programUsers.findOne(query).lean() + console.error('updateEntity - Document still exists:', docStillExists ? true : false) + return reject({ message: 'Failed to update entity', status: 500, + details: { + updateQuery, + setOperations, + entityId, + matchField, + }, }) } return resolve(result) } catch (error) { + console.error('updateEntity - Exception caught:', { + error: error.message || error, + stack: error.stack, + userId, + programId, + programExternalId, + entityId, + entityUpdates: JSON.stringify(entityUpdates), + }) return reject(error) } }) diff --git a/generics/kafka/producers.js b/generics/kafka/producers.js index 63e806f1..5000148a 100644 --- a/generics/kafka/producers.js +++ b/generics/kafka/producers.js @@ -106,10 +106,10 @@ const pushMessageToKafka = function (payload) { throw reject('Kafka configuration is not done') } - console.log('-------Kafka producer log starts here------------------') - console.log('Topic Name: ', payload[0].topic) - console.log('Message: ', JSON.stringify(payload)) - console.log('-------Kafka producer log ends here------------------') + // console.log('-------Kafka producer log starts here------------------') + // console.log('Topic Name: ', payload[0].topic) + // console.log('Message: ', JSON.stringify(payload)) + // console.log('-------Kafka producer log ends here------------------') kafkaClient.kafkaProducer.send(payload, (err, data) => { if (err) { diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 643faffa..5b6ad7c9 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -4563,8 +4563,22 @@ module.exports = class UserProjectsHelper { const masterProgramId = programAndSolutionInformation.result.program._id const masterSolutionId = programAndSolutionInformation.result.solution._id + let acl = {} + if (userDetails.userInformation.userId != participantId) { + acl = { + visibility: CONSTANTS.common.PROJECT_VISIBILITY_SPECIFIC, + users: [participantId, userDetails.userInformation.userId], + } + } else { + acl = { + visibility: CONSTANTS.common.PROJECT_VISIBILITY_SELF, + users: [participantId], + } + } + // Step 3: Initialize Single Project let projectData = { + acl: acl, title: projectConfig?.name || `${userProfile.data.name}'s Project Plan`, description: projectConfig?.description || 'Individual Development Plan', userId: participantId, @@ -4590,6 +4604,8 @@ module.exports = class UserProjectsHelper { tenantId: tenantId, orgId: orgId, categories: allCategories, // Add categories from all templates + keywords: ['project-plan'], + referenceFrom: projectConfig?.referenceFrom || '', tasks: [], taskSequence: [], userProfile: userProfile.data, From 86b20212cee9890c88e5e4758f54f6d808e625bc Mon Sep 17 00:00:00 2001 From: vaishali k Date: Thu, 22 Jan 2026 12:13:57 +0530 Subject: [PATCH 32/44] Issue #000 fix: user activity kafka consumer added which will listen when project is assigned to a user through importLibrary or createProject Plan --- databaseQueries/programUsers.js | 32 ------ generics/kafka/consumers/userActivities.js | 110 +++++++++++++++++++++ 2 files changed, 110 insertions(+), 32 deletions(-) create mode 100644 generics/kafka/consumers/userActivities.js diff --git a/databaseQueries/programUsers.js b/databaseQueries/programUsers.js index a09a3895..eb536868 100644 --- a/databaseQueries/programUsers.js +++ b/databaseQueries/programUsers.js @@ -432,19 +432,6 @@ module.exports = class programUsers { const updateQuery = { ...query } updateQuery[matchField] = entityId - // Log for debugging - console.log('updateEntity - Debug Info:', { - userId, - programId, - programExternalId, - entityId, - matchField, - updateQuery: JSON.stringify(updateQuery), - setOperations: JSON.stringify(setOperations), - entityFound: entity ? true : false, - entityData: entity ? JSON.stringify(entity) : null, - }) - // Perform the update const result = await database.models.programUsers.findOneAndUpdate( updateQuery, @@ -455,25 +442,6 @@ module.exports = class programUsers { ) if (!result) { - // Enhanced error logging - console.error('updateEntity - Update failed:', { - message: 'findOneAndUpdate returned null', - updateQuery: JSON.stringify(updateQuery), - setOperations: JSON.stringify(setOperations), - userId, - programId, - programExternalId, - entityId, - matchField, - originalQuery: JSON.stringify(query), - docDataExists: docData ? true : false, - entityExists: entity ? true : false, - }) - - // Check if document still exists - const docStillExists = await database.models.programUsers.findOne(query).lean() - console.error('updateEntity - Document still exists:', docStillExists ? true : false) - return reject({ message: 'Failed to update entity', status: 500, diff --git a/generics/kafka/consumers/userActivities.js b/generics/kafka/consumers/userActivities.js new file mode 100644 index 00000000..4cee8a11 --- /dev/null +++ b/generics/kafka/consumers/userActivities.js @@ -0,0 +1,110 @@ +/** + * name : submissions.js + * author : Aman Jung Karki + * created-date : 22-Nov-2020 + * Description : Submission consumer. + */ + +//dependencies + +const programUsersService = require(SERVICES_BASE_PATH + '/programUsers') +const ObjectId = global.ObjectId || require('mongoose').Types.ObjectId + +/** + * 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 { + let parsedMessage = JSON.parse(message.value) + let userId = parsedMessage.userId + let project = parsedMessage.projects + let createdBy = project.createdBy + let projectProgramId = project.programId + let programExternalId = project.programExternalId + let hierarchy = [] + if (userId != createdBy) { + hierarchy.push({ + level: 0, + id: createdBy, + }) + } + //let programUsersRef = await programUsersService.findByUserAndProgram(userId, projectProgramId); + //if (!programUsersRef) { + let result = await programUsersService.createOrUpdate({ + userId: userId, + hierarchy: hierarchy, + programId: projectProgramId, + programExternalId: project.programExternalId, + entities: [], + status: 'IN_PROGRESS', + metaInformation: { + idpAssignedAt: new Date(), + idpAssignedBy: createdBy, + idpProjectId: project._id, + }, + createdBy: createdBy, + updatedBy: createdBy, + referenceFrom: new ObjectId(project.referenceFrom), + tenantId: project.tenantId, + orgId: project.orgId, + }) + + if (result.result._id) { + console.log('PARTICIPANT_PROGRAMUSERS_ASSIGNED', result.result._id) + if (project.referenceFrom) { + let result2 = await programUsersService.updateEntity( + createdBy, + project.referenceFrom, + '', + `${userId}`, + { + status: 'IN_PROGRESS', + participantProgramUserReference: result.result._id, + }, + project.tenantId + ) + if (result2._id) { + console.log('LC_PROGRAMUSERS_ENTITY_UPDATED', result2._id) + } else { + console.log('LC_PROGRAMUSERS_ENTITY_ASSIGNMENT_FAILED', result2) + } + } + } else { + console.log('PARTICIPANT_PROGRAMUSERS_ASSIGNMENT_FAILED', result.result) + } + + 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, +} From 295169171246ebeabbdbd7f3269cda09677bbd86 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Thu, 22 Jan 2026 15:19:49 +0530 Subject: [PATCH 33/44] Issue #000 fix: user activity kafka consumer removed as it was async --- generics/kafka/consumers/userActivities.js | 112 ++++++++++----------- module/userProjects/helper.js | 82 +++++++++++++++ 2 files changed, 138 insertions(+), 56 deletions(-) diff --git a/generics/kafka/consumers/userActivities.js b/generics/kafka/consumers/userActivities.js index 4cee8a11..f27cd936 100644 --- a/generics/kafka/consumers/userActivities.js +++ b/generics/kafka/consumers/userActivities.js @@ -21,63 +21,63 @@ const ObjectId = global.ObjectId || require('mongoose').Types.ObjectId var messageReceived = function (message) { return new Promise(async function (resolve, reject) { try { - let parsedMessage = JSON.parse(message.value) - let userId = parsedMessage.userId - let project = parsedMessage.projects - let createdBy = project.createdBy - let projectProgramId = project.programId - let programExternalId = project.programExternalId - let hierarchy = [] - if (userId != createdBy) { - hierarchy.push({ - level: 0, - id: createdBy, - }) - } - //let programUsersRef = await programUsersService.findByUserAndProgram(userId, projectProgramId); - //if (!programUsersRef) { - let result = await programUsersService.createOrUpdate({ - userId: userId, - hierarchy: hierarchy, - programId: projectProgramId, - programExternalId: project.programExternalId, - entities: [], - status: 'IN_PROGRESS', - metaInformation: { - idpAssignedAt: new Date(), - idpAssignedBy: createdBy, - idpProjectId: project._id, - }, - createdBy: createdBy, - updatedBy: createdBy, - referenceFrom: new ObjectId(project.referenceFrom), - tenantId: project.tenantId, - orgId: project.orgId, - }) + // let parsedMessage = JSON.parse(message.value) + // let userId = parsedMessage.userId + // let project = parsedMessage.projects + // let createdBy = project.createdBy + // let projectProgramId = project.programId + // let programExternalId = project.programExternalId + // let hierarchy = [] + // if (userId != createdBy) { + // hierarchy.push({ + // level: 0, + // id: createdBy, + // }) + // } + // //let programUsersRef = await programUsersService.findByUserAndProgram(userId, projectProgramId); + // //if (!programUsersRef) { + // let result = await programUsersService.createOrUpdate({ + // userId: userId, + // hierarchy: hierarchy, + // programId: projectProgramId, + // programExternalId: project.programExternalId, + // entities: [], + // status: 'IN_PROGRESS', + // metaInformation: { + // idpAssignedAt: new Date(), + // idpAssignedBy: createdBy, + // idpProjectId: project._id, + // }, + // createdBy: createdBy, + // updatedBy: createdBy, + // referenceFrom: new ObjectId(project.referenceFrom), + // tenantId: project.tenantId, + // orgId: project.orgId, + // }) - if (result.result._id) { - console.log('PARTICIPANT_PROGRAMUSERS_ASSIGNED', result.result._id) - if (project.referenceFrom) { - let result2 = await programUsersService.updateEntity( - createdBy, - project.referenceFrom, - '', - `${userId}`, - { - status: 'IN_PROGRESS', - participantProgramUserReference: result.result._id, - }, - project.tenantId - ) - if (result2._id) { - console.log('LC_PROGRAMUSERS_ENTITY_UPDATED', result2._id) - } else { - console.log('LC_PROGRAMUSERS_ENTITY_ASSIGNMENT_FAILED', result2) - } - } - } else { - console.log('PARTICIPANT_PROGRAMUSERS_ASSIGNMENT_FAILED', result.result) - } + // if (result.result._id) { + // console.log('PARTICIPANT_PROGRAMUSERS_ASSIGNED', result.result._id) + // if (project.referenceFrom) { + // let result2 = await programUsersService.updateEntity( + // createdBy, + // project.referenceFrom, + // '', + // `${userId}`, + // { + // status: 'IN_PROGRESS', + // participantProgramUserReference: result.result._id, + // }, + // project.tenantId + // ) + // if (result2._id) { + // console.log('LC_PROGRAMUSERS_ENTITY_UPDATED', result2._id) + // } else { + // console.log('LC_PROGRAMUSERS_ENTITY_ASSIGNMENT_FAILED', result2) + // } + // } + // } else { + // console.log('PARTICIPANT_PROGRAMUSERS_ASSIGNMENT_FAILED', result.result) + // } return resolve('Message Received') } catch (error) { diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 5b6ad7c9..0b3bd8af 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -36,6 +36,8 @@ const projectService = require(SERVICES_BASE_PATH + '/projects') const defaultUserProfileConfig = require('@config/defaultUserProfileDeleteConfig') const configFilePath = process.env.AUTH_CONFIG_FILE_PATH const surveyService = require(SERVICES_BASE_PATH + '/survey') +const programUsersService = require(SERVICES_BASE_PATH + '/programUsers') +const ObjectId = global.ObjectId || require('mongoose').Types.ObjectId /** * UserProjectsHelper @@ -4869,6 +4871,11 @@ module.exports = class UserProjectsHelper { // Push to Kafka for event streaming await this.attachEntityInformationIfExists(createdProject) + let programUserMapping = await this.handleProgramUserMapping({ + userId: participantId, + project: createdProject, + }) + console.log('Program User Mapping Result:*****************', programUserMapping) await kafkaProducersHelper.pushProjectToKafka(createdProject) await kafkaProducersHelper.pushUserActivitiesToKafka({ userId: participantId, @@ -4894,6 +4901,81 @@ module.exports = class UserProjectsHelper { }) } + /*Add user to program user mapping on project creation*/ + static async handleProgramUserMapping(eventData) { + return new Promise(async (resolve, reject) => { + try { + let { userId, project } = eventData + let createdBy = project.createdBy + let projectProgramId = project.programId + let programExternalId = project.programExternalId + let hierarchy = [] + if (userId != createdBy) { + hierarchy.push({ + level: 0, + id: createdBy, + }) + } + //let programUsersRef = await programUsersService.findByUserAndProgram(userId, projectProgramId); + //if (!programUsersRef) { + let result = await programUsersService.createOrUpdate({ + userId: userId, + hierarchy: hierarchy, + programId: projectProgramId, + programExternalId: programExternalId, + entities: [], + status: 'IN_PROGRESS', + metaInformation: { + idpAssignedAt: new Date(), + idpAssignedBy: createdBy, + idpProjectId: project._id, + }, + createdBy: createdBy, + updatedBy: createdBy, + referenceFrom: new ObjectId(project.referenceFrom), + tenantId: project.tenantId, + orgId: project.orgId, + }) + + if (result.result._id) { + console.log('PARTICIPANT_PROGRAMUSERS_ASSIGNED', result.result._id) + if (project.referenceFrom) { + let result2 = await programUsersService.updateEntity( + createdBy, + project.referenceFrom, + '', + `${userId}`, + { + status: 'IN_PROGRESS', + idpProjectId: project._id, + participantProgramUserReference: result.result._id, + }, + project.tenantId + ) + if (result2._id) { + console.log('LC_PROGRAMUSERS_ENTITY_UPDATED', result2._id) + } else { + console.log('LC_PROGRAMUSERS_ENTITY_ASSIGNMENT_FAILED', result2) + } + } + } else { + console.log('PARTICIPANT_PROGRAMUSERS_ASSIGNMENT_FAILED', result.result) + } + + return resolve({ + success: true, + message: 'Program user mapping handled successfully', + result: result.result, + }) + } catch (error) { + return reject({ + success: false, + status: error.status ? error.status : HTTP_STATUS_CODE.internal_server_error.status, + message: error.message || error, + }) + } + }) + } /** * Get project infromation when project as a task * @method From c6591899d8b87f34aaf04d6544b5b926d14ddd84 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Fri, 23 Jan 2026 11:34:48 +0530 Subject: [PATCH 34/44] Issue #000 fix: check for program reference --- module/userProjects/helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 0b3bd8af..8199f0bb 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -4932,7 +4932,7 @@ module.exports = class UserProjectsHelper { }, createdBy: createdBy, updatedBy: createdBy, - referenceFrom: new ObjectId(project.referenceFrom), + referenceFrom: project.referenceFrom ? new ObjectId(project.referenceFrom) : null, tenantId: project.tenantId, orgId: project.orgId, }) From 9650f1295ead0398c4695471e94f9042d9b2ad57 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Fri, 23 Jan 2026 12:05:14 +0530 Subject: [PATCH 35/44] Issue #000 fix: remove timestamp from the name of the child observation --- module/userProjects/helper.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 8199f0bb..4e941247 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -5447,9 +5447,9 @@ async function _projectTask( singleTask.solutionDetails._id, '', { - name: `${singleTask.solutionDetails.name}-${timestamp}`, + name: `${singleTask.solutionDetails.name}`, externalId: `${singleTask.solutionDetails.externalId}-${timestamp}`, - description: `${singleTask.solutionDetails.name}-${timestamp}`, + description: `${singleTask.solutionDetails.name}`, programExternalId: programId, status: CONSTANTS.common.PUBLISHED_STATUS, tenantData: userDetails.tenantAndOrgInfo, From 1c17d801d4955f94bd73f2a90267e5c69cb00f8c Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:07:46 +0530 Subject: [PATCH 36/44] Issue#252622 Feat: Importing project from multiple templates --- module/userProjects/helper.js | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 198528e1..2723b8d1 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -4688,6 +4688,11 @@ module.exports = class UserProjectsHelper { // e. Process Custom Tasks if provided let processedCustomTasks = [] if (template.customTasks && template.customTasks.length > 0) { + // Preserve metaInformation from original request before processing + const originalMetaInformation = template.customTasks.map((task) => { + return task && task.metaInformation ? { ...task.metaInformation } : null + }) + // Ensure all custom tasks have _id before processing const customTasksWithIds = template.customTasks .map((task) => { @@ -4717,13 +4722,33 @@ module.exports = class UserProjectsHelper { processedCustomTasks = [] } - // Mark all custom tasks as isACustomTask: true - processedCustomTasks.forEach((customTask) => { + // Mark all custom tasks as isACustomTask: true and add metaInformation + processedCustomTasks.forEach((customTask, index) => { customTask.isACustomTask = true customTask.createdBy = userId customTask.updatedBy = userId customTask.createdAt = new Date() customTask.updatedAt = new Date() + + // Add metaInformation for custom tasks + if (!customTask.metaInformation) { + customTask.metaInformation = {} + } + + // Use metaInformation from original request if present, else use defaults + const originalMeta = originalMetaInformation[index] + if (originalMeta) { + // Merge original metaInformation with processed task's metaInformation + customTask.metaInformation.buttonLabel = + originalMeta.buttonLabel || customTask.metaInformation.buttonLabel || 'Upload' + customTask.metaInformation.icon = + originalMeta.icon || customTask.metaInformation.icon || 'Upload' + } else { + // No metaInformation in original request, use defaults + customTask.metaInformation.buttonLabel = + customTask.metaInformation.buttonLabel || 'Upload' + customTask.metaInformation.icon = customTask.metaInformation.icon || 'Upload' + } }) } From 28830cd77e1eacd0ea1cf7521056f23035a62d71 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Tue, 27 Jan 2026 14:14:54 +0530 Subject: [PATCH 37/44] Issue #000 fix: add templates and acl to the project plan --- generics/helpers/utils.js | 69 +++++++++++++++++++++++++++++++++++ models/programUsers.js | 4 +- models/projects.js | 29 +++++++++++++++ module/userProjects/helper.js | 20 ++++++---- 4 files changed, 113 insertions(+), 9 deletions(-) diff --git a/generics/helpers/utils.js b/generics/helpers/utils.js index bbb5099a..70abebdc 100644 --- a/generics/helpers/utils.js +++ b/generics/helpers/utils.js @@ -1078,4 +1078,73 @@ module.exports = { strictObjectIdCheck, upperCase: upperCase, addSolutionsWithOrdersForProgramComponent, + /** + * Sync project template fields between old (single) and new (array) formats + * This ensures backward compatibility when reading/writing projects + * @param {Object} projectData - Project data object + * @param {String} direction - 'normalize' (old->new) or 'legacy' (new->old) or 'sync' (both ways) + * @returns {Object} Project data with synced template fields + */ + syncProjectTemplates: function (projectData) { + if (!projectData || typeof projectData !== 'object') { + return projectData + } + + const ObjectId = global.ObjectId || require('mongoose').Types.ObjectId + + if ( + (projectData.projectTemplateId || projectData.projectTemplateExternalId) && + (!projectData.projectTemplates || projectData.projectTemplates.length === 0) + ) { + projectData.projectTemplates = [] + if (projectData.projectTemplateId) { + projectData.projectTemplates.push({ + _id: + projectData.projectTemplateId instanceof ObjectId + ? projectData.projectTemplateId + : new ObjectId(projectData.projectTemplateId), + externalId: projectData.projectTemplateExternalId || '', + addedAt: new Date(), + }) + } + } + + return projectData + }, + + /** + * Build query to search projects by template ID (supports both legacy and new formats) + * @param {String|ObjectId|Array} templateIds - Single template ID or array of template IDs + * @param {Object} additionalFilters - Additional query filters (e.g., { tenantId, orgId, status }) + * @returns {Object} MongoDB query object + */ + buildProjectTemplateQuery: function (templateIds, additionalFilters = {}) { + if (!templateIds) { + return additionalFilters + } + + const ObjectId = global.ObjectId || require('mongoose').Types.ObjectId + + // Normalize templateIds to array + const templateIdsArray = Array.isArray(templateIds) ? templateIds : [templateIds] + + // Convert to ObjectIds + const templateObjectIds = templateIdsArray.map((id) => (id instanceof ObjectId ? id : new ObjectId(id))) + + // Build query that searches both legacy and new formats + const templateQuery = { + $or: [ + // Legacy single template field + { projectTemplateId: { $in: templateObjectIds } }, + // New multiple templates array field + { 'projectTemplates._id': { $in: templateObjectIds } }, + ], + } + + // Merge with additional filters + return { + ...additionalFilters, + ...templateQuery, + } + }, } diff --git a/models/programUsers.js b/models/programUsers.js index 7b8cdb43..145db970 100644 --- a/models/programUsers.js +++ b/models/programUsers.js @@ -82,9 +82,9 @@ module.exports = { }, // ============================================ - // TAGS (OPTIONAL - FOR FILTERING/SEARCH) + // keywords (OPTIONAL - FOR FILTERING/SEARCH) // ============================================ - tags: { + keywords: { type: [String], default: [], description: 'Optional tags for categorization and search', diff --git a/models/projects.js b/models/projects.js index d4995fed..d49e6cc1 100644 --- a/models/projects.js +++ b/models/projects.js @@ -75,6 +75,7 @@ module.exports = { type: String, default: 'SYSTEM', }, + // Legacy single template fields (kept for backward compatibility) projectTemplateId: { type: 'ObjectId', index: true, @@ -83,6 +84,29 @@ module.exports = { type: String, index: true, }, + // New multiple templates support + projectTemplates: { + type: [ + { + _id: { + type: 'ObjectId', + required: true, + }, + externalId: { + type: String, + required: true, + }, + }, + ], + default: [], + description: 'Array of project templates associated with this project', + }, + // keywords (OPTIONAL - FOR FILTERING/SEARCH) + keywords: { + type: [String], + default: [], + description: 'Optional tags for categorization and search', + }, startDate: Date, endDate: Date, learningResources: { @@ -226,5 +250,10 @@ module.exports = { name: { userId: 1, solutionId: 1 }, indexType: { unique: true, partialFilterExpression: { solutionId: { $exists: true } } }, }, + // Index for querying by template ID in array (multiple templates) + { + name: { 'projectTemplates._id': 1 }, + indexType: {}, + }, ], } diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 4e941247..9f325b5c 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -4503,6 +4503,13 @@ module.exports = class UserProjectsHelper { ['_id', 'title', 'categories', 'solutionId', 'solutionExternalId', 'externalId', 'taskSequence'] ) + if (validTemplates.length !== templates.length) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.PROJECT_TEMPLATE_NOT_FOUND, + } + } + // Collect all unique category IDs from templates let allCategoryIds = [] const categoryIdSet = new Set() // To avoid duplicates @@ -4530,13 +4537,6 @@ module.exports = class UserProjectsHelper { ) } - if (validTemplates.length !== templates.length) { - throw { - status: HTTP_STATUS_CODE.bad_request.status, - message: CONSTANTS.apiResponses.PROJECT_TEMPLATE_NOT_FOUND, - } - } - // Step 2: Always Create New Program & Solution let programAndSolutionData = { programName: programName || `Program for ${userProfile.data.name}`, @@ -4615,6 +4615,12 @@ module.exports = class UserProjectsHelper { updatedAt: new Date(), } + // Add project templates to project data + projectData.projectTemplates = validTemplates.map((template) => ({ + _id: template._id instanceof global.ObjectId ? template._id : new global.ObjectId(template._id), + externalId: template.externalId, + })) + // Add entity information if provided if (entityId) { const entityInfo = await entitiesService.entityDocuments( From cf6ff1452d8764b424762e8ff0d025350b83c766 Mon Sep 17 00:00:00 2001 From: vaishali k Date: Tue, 27 Jan 2026 15:48:05 +0530 Subject: [PATCH 38/44] Issue #000 fix: no overview count is sent if assigned users with specific status are not found --- generics/services/programUsers.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/generics/services/programUsers.js b/generics/services/programUsers.js index 15a73dc0..1744b974 100644 --- a/generics/services/programUsers.js +++ b/generics/services/programUsers.js @@ -164,9 +164,10 @@ module.exports = class ProgramUsersService { ) if (filteredEntities.length === 0) { return { - status: 404, + status: 200, message: 'Entity not found', - data: [], + data: { data: [], overview: docData.overview || {} }, + result: { data: [], overview: docData.overview || {} }, } } } @@ -196,10 +197,12 @@ module.exports = class ProgramUsersService { // Throw error if no valid users returned from service if (!success || !data || data.count === 0) { - throw { - success: false, - status: HTTP_STATUS_CODE.bad_request.status, + return { + success: true, + status: 200, message: 'No valid users found for the provided entity user IDs.', + data: { data: [], overview: docData.overview || {} }, + result: { data: [], overview: docData.overview || {} }, } } From 5a7aff76edacc489f4c7cf18359c484c7ef7a7a0 Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:11:06 +0530 Subject: [PATCH 39/44] Issue#253042 Fix: Update task status for observation type task complete --- module/userProjects/helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 7d1304b8..39dcd79d 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -375,7 +375,7 @@ module.exports = class UserProjectsHelper { if (data.tasks) { let taskReport = {} - updateProject.tasks = await _projectTask(data.tasks) + updateProject.tasks = await _projectTask(data.tasks, false, '', '', data.programId, userDetails) if (userProject[0].tasks && userProject[0].tasks.length > 0) { updateProject.tasks.forEach((task) => { From 625dd014dff2880af22ff76d19ca2e8b3a104c92 Mon Sep 17 00:00:00 2001 From: "abhilash.dube" Date: Thu, 29 Jan 2026 17:17:23 +0530 Subject: [PATCH 40/44] Updated the Dev Pipeline and Added The QA Pipeline --- .github/workflows/brac-dev-deployment.yaml | 6 +- .github/workflows/brac-qa-deplyment.yaml | 87 ++++++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/brac-qa-deplyment.yaml diff --git a/.github/workflows/brac-dev-deployment.yaml b/.github/workflows/brac-dev-deployment.yaml index f390be26..8b00856d 100644 --- a/.github/workflows/brac-dev-deployment.yaml +++ b/.github/workflows/brac-dev-deployment.yaml @@ -56,9 +56,9 @@ jobs: uses: appleboy/ssh-action@master with: host: ${{ secrets.HOST_NAME_DEV }} - username: ${{ secrets.USERNAME_DEV }} - key: ${{ secrets.SSH_KEY_DEV }} - port: ${{ secrets.PORT_DEV }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.SSH_KEY }} + port: ${{ secrets.PORT }} script: | set -e diff --git a/.github/workflows/brac-qa-deplyment.yaml b/.github/workflows/brac-qa-deplyment.yaml new file mode 100644 index 00000000..f82fc2d0 --- /dev/null +++ b/.github/workflows/brac-qa-deplyment.yaml @@ -0,0 +1,87 @@ +name: Tag Build & Deploy User Service (BRAC) + +on: + push: + tags: + - "v*" + +env: + AWS_REGION: ${{ secrets.AWS_REGION }} + AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} + ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY_BRAC }} + TAG: ${{ github.ref_name }} + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # ========================= + # AWS Authentication + # ========================= + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + # ========================= + # Login to Amazon ECR + # ========================= + - name: Login to Amazon ECR + uses: aws-actions/amazon-ecr-login@v2 + + # ========================= + # Build & Push Docker Image + # ========================= + - name: Build and Push Docker Image to ECR + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}:${{ env.TAG }} + + # ========================= + # Deploy on QA Server + # ========================= + - name: Deploy Stack to QA + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.HOST_NAME_QA }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.SSH_KEY }} + port: ${{ secrets.PORT }} + script: | + set -e + + export AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} + export AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} + export AWS_REGION=${{ env.AWS_REGION }} + + cd ${{ secrets.TARGET_DIR_QA }} + + # Backup old env if exists + if [ -f .env ]; then + mv .env .env-bkp + fi + + # Write env safely (MULTILINE SAFE) + cat << 'EOF' > .env + ${{ secrets.QA_ENV_BRAC }} + EOF + + aws ecr get-login-password --region ${AWS_REGION} \ + | docker login \ + --username AWS \ + --password-stdin \ + ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${AWS_REGION}.amazonaws.com + + ./deploy.sh ${{ env.TAG }} From 7772b1b66f435fc9e4ad32c7a5758072c98daf74 Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:55:04 +0530 Subject: [PATCH 41/44] Issue#233138 Fix: Replace import from pdf function to the detailsv2 function --- module/solutions/helper.js | 24 +++++++++++++++++++++--- module/userProjects/helper.js | 8 +++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/module/solutions/helper.js b/module/solutions/helper.js index 5083ebce..87760607 100644 --- a/module/solutions/helper.js +++ b/module/solutions/helper.js @@ -701,16 +701,34 @@ module.exports = class SolutionsHelper { Object.keys( _.omit(data, ['role', 'filter', 'factors', 'type', 'tenantId', 'orgId', 'organizations']) ).forEach((key) => { - data[key] = data[key].split(',') + // Only split if the value exists and is a string + if (typeof data[key] === 'string') { + data[key] = data[key].split(',') + } }) // If validate entity set to ON . strict scoping should be applied Object.keys( _.omit(data, ['filter', 'role', 'factors', 'type', 'tenantId', 'orgId', 'organizations']) ).forEach((requestedDataKey) => { - registryIds.push(...data[requestedDataKey]) + // 1. Get the value + const value = data[requestedDataKey] + + // 2. Ensure it is an array before spreading + // If it's a string, split it. If it's already an array, use it. Otherwise, use an empty array. + const normalizedArray = Array.isArray(value) + ? value + : typeof value === 'string' + ? value.split(',') + : [] + + // 3. Spread safely + registryIds.push(...normalizedArray) entityTypes.push(requestedDataKey) }) - if (!registryIds.length > 0) { + + // Minor logic fix: "if (!registryIds.length > 0)" is a bit messy. + // Use "if (registryIds.length === 0)" for clarity. + if (registryIds.length === 0) { throw { message: CONSTANTS.apiResponses.NO_LOCATION_ID_FOUND_IN_DATA, } diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 39dcd79d..5137093c 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -1648,7 +1648,9 @@ module.exports = class UserProjectsHelper { projectTemplateId, userId, tenantId, - orgId + orgId, + userDetails, + solutionDetails.programId ) if (!projectCreation.success) { return resolve(projectCreation) @@ -2070,7 +2072,7 @@ module.exports = class UserProjectsHelper { * @returns {String} - message. */ - static userAssignedProjectCreation(templateId, userId, tenantId, orgId) { + static userAssignedProjectCreation(templateId, userId, tenantId, orgId, userDetails = {}, programId = '') { return new Promise(async (resolve, reject) => { try { const projectTemplateData = await projectTemplateQueries.templateDocument( @@ -2112,7 +2114,7 @@ module.exports = class UserProjectsHelper { orgId ) if (tasksAndSubTasks.length > 0) { - result.tasks = await _projectTask(tasksAndSubTasks) + result.tasks = await _projectTask(tasksAndSubTasks, false, '', '', programId, userDetails) result.tasks.forEach((task) => { if ( task && From 3fdbe4b78862e6b28011578872286fd089c780df Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:44:18 +0530 Subject: [PATCH 42/44] Issue#233138 Fix: Replace import from pdf function to the detailsv2 function --- module/solutions/helper.js | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/module/solutions/helper.js b/module/solutions/helper.js index 87760607..5083ebce 100644 --- a/module/solutions/helper.js +++ b/module/solutions/helper.js @@ -701,34 +701,16 @@ module.exports = class SolutionsHelper { Object.keys( _.omit(data, ['role', 'filter', 'factors', 'type', 'tenantId', 'orgId', 'organizations']) ).forEach((key) => { - // Only split if the value exists and is a string - if (typeof data[key] === 'string') { - data[key] = data[key].split(',') - } + data[key] = data[key].split(',') }) // If validate entity set to ON . strict scoping should be applied Object.keys( _.omit(data, ['filter', 'role', 'factors', 'type', 'tenantId', 'orgId', 'organizations']) ).forEach((requestedDataKey) => { - // 1. Get the value - const value = data[requestedDataKey] - - // 2. Ensure it is an array before spreading - // If it's a string, split it. If it's already an array, use it. Otherwise, use an empty array. - const normalizedArray = Array.isArray(value) - ? value - : typeof value === 'string' - ? value.split(',') - : [] - - // 3. Spread safely - registryIds.push(...normalizedArray) + registryIds.push(...data[requestedDataKey]) entityTypes.push(requestedDataKey) }) - - // Minor logic fix: "if (!registryIds.length > 0)" is a bit messy. - // Use "if (registryIds.length === 0)" for clarity. - if (registryIds.length === 0) { + if (!registryIds.length > 0) { throw { message: CONSTANTS.apiResponses.NO_LOCATION_ID_FOUND_IN_DATA, } From 4884e5e7284e0a7c055ee56148074dee52a04675 Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:48:56 +0530 Subject: [PATCH 43/44] Issue#253202 Feat:Functionality for create project from template use only selected tasks --- module/userProjects/helper.js | 43 +++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 39dcd79d..37cfca61 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -4681,8 +4681,42 @@ module.exports = class UserProjectsHelper { continue } + let excludedExternalIds = [] + let filteredTemplateTasks = templateTasks + if ( + template.excludedTaskIds && + Array.isArray(template.excludedTaskIds) && + template.excludedTaskIds.length > 0 + ) { + // Create a map for quick task lookup + const templateTaskMap = new Map(templateTasks.map((task) => [task._id.toString(), task])) + + for (const taskId of template.excludedTaskIds) { + const task = templateTaskMap.get(taskId.toString()) + if (!task) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: `Task ID ${taskId} not found in template ${template.templateId}`, + } + } + + const isDeletable = task.hasOwnProperty('isDeletable') ? task.isDeletable : false + if (!isDeletable) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: `Task ${task.name} (${taskId}) is not deletable and cannot be excluded`, + } + } + excludedExternalIds.push(task.externalId) + } + + filteredTemplateTasks = templateTasks.filter( + (task) => !template.excludedTaskIds.includes(task._id.toString()) + ) + } + // Ensure all tasks have _id before processing (required by _projectTask) - const tasksWithIds = templateTasks + const tasksWithIds = filteredTemplateTasks .map((task) => { if (task && !task._id) { task._id = uuidv4() @@ -4803,6 +4837,11 @@ module.exports = class UserProjectsHelper { // If template has taskSequence, use it to order the subtasks if (templateData.taskSequence && templateData.taskSequence.length > 0) { + // Filter out excluded external IDs from template's taskSequence + const filteredTemplateTaskSequence = templateData.taskSequence.filter( + (extId) => !excludedExternalIds.includes(extId) + ) + // Create a map of externalId to task for quick lookup const taskMap = new Map() allSubTasks.forEach((task) => { @@ -4812,7 +4851,7 @@ module.exports = class UserProjectsHelper { }) // First, add tasks in template's taskSequence order - templateData.taskSequence.forEach((templateTaskExternalId) => { + filteredTemplateTaskSequence.forEach((templateTaskExternalId) => { const task = taskMap.get(templateTaskExternalId) if (task && task.externalId) { improvementTaskSequence.push(task.externalId) From 7f1cbe619a2cdc1589e21563cc1383277bf5111b Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:25:28 +0530 Subject: [PATCH 44/44] Issue#253202 Feat: userId is not explicitly provided in the request --- controllers/v1/programUsers.js | 19 ++++++++++++++++++- module/programUsers/validator/v1.js | 14 -------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/controllers/v1/programUsers.js b/controllers/v1/programUsers.js index eed9fd7f..889de1e8 100644 --- a/controllers/v1/programUsers.js +++ b/controllers/v1/programUsers.js @@ -32,6 +32,14 @@ module.exports = class ProgramUsers extends Abstract { async createOrUpdate(req) { return new Promise(async (resolve, reject) => { try { + if ( + !req.body.userId && + req.userDetails && + req.userDetails.userInformation && + req.userDetails.userInformation.userId + ) { + req.body.userId = req.userDetails.userInformation.userId + } // Check if this is an entity-specific update if (req.body.entityId && req.body.entityUpdates) { const result = await programUsersHelper.updateEntity(req.body, req.userDetails) @@ -61,9 +69,18 @@ module.exports = class ProgramUsers extends Abstract { async getEntities(req) { return new Promise(async (resolve, reject) => { try { - const { userId, programId, programExternalId, status, search = '', entityId = '' } = req.query + let { userId, programId, programExternalId, status, search = '', entityId = '' } = req.query const { pageNo = 1, pageSize = 20 } = req + if ( + !userId && + req.userDetails && + req.userDetails.userInformation && + req.userDetails.userInformation.userId + ) { + userId = req.userDetails.userInformation.userId + } + if (!userId || (!programId && !programExternalId)) { return reject({ status: HTTP_STATUS_CODE.bad_request.status, diff --git a/module/programUsers/validator/v1.js b/module/programUsers/validator/v1.js index 3c927b63..12277db7 100644 --- a/module/programUsers/validator/v1.js +++ b/module/programUsers/validator/v1.js @@ -8,13 +8,6 @@ module.exports = (req) => { let programUsersValidator = { createOrUpdate: function () { - // Validate required fields - req.checkBody('userId') - .exists() - .withMessage('userId is required') - .isString() - .withMessage('userId must be a string') - // Either programId or programExternalId is required req.checkBody('programId') .custom((value) => { @@ -108,13 +101,6 @@ module.exports = (req) => { }, getEntities: function () { - // Validate required query parameters - req.checkQuery('userId') - .exists() - .withMessage('userId is required') - .isString() - .withMessage('userId must be a string') - // Either programId or programExternalId is required req.checkQuery('programId') .custom((value) => {