From 99220ae906e56c0eddcc3678c725ecfcf391e814 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 9 Jul 2025 22:05:46 +0200 Subject: [PATCH 001/105] fix: make notes mandatory --- .circleci/config.yml | 2 +- src/routes/copilotOpportunityApply/create.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ff93f090..4e13e921 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'pm-1378'] + only: ['develop', 'migration-setup', 'pm-1468'] - deployProd: context : org-global filters: diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index 4bfbea9f..5c2f8346 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -11,7 +11,7 @@ import { createEvent } from '../../services/busApi'; const applyCopilotRequestValidations = { body: Joi.object().keys({ - notes: Joi.string().optional(), + notes: Joi.string().required(), }), }; From 8f2fbd5e9574210d34828511d56da67567e4ef7a Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 17 Jul 2025 21:16:25 +0200 Subject: [PATCH 002/105] fix: group by active and then sort by createdAt --- .circleci/config.yml | 2 +- src/routes/copilotOpportunity/list.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4e13e921..83db4e29 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'pm-1468'] + only: ['develop', 'migration-setup', 'pm-1368'] - deployProd: context : org-global filters: diff --git a/src/routes/copilotOpportunity/list.js b/src/routes/copilotOpportunity/list.js index 772eba28..59fd5dc1 100644 --- a/src/routes/copilotOpportunity/list.js +++ b/src/routes/copilotOpportunity/list.js @@ -34,7 +34,10 @@ module.exports = [ attributes: ['name'], }, ], - order: [[sortParams[0], sortParams[1]]], + order: [ + [models.Sequelize.literal(`CASE WHEN status = 'active' THEN 0 ELSE 1 END`), 'ASC'], + [sortParams[0], sortParams[1]] + ], limit, offset, }) From 2218b14c150dc81c17b953fbdc93e05e032d5e07 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 17 Jul 2025 21:44:18 +0200 Subject: [PATCH 003/105] fix: group by active and then sort by createdAt --- src/routes/copilotOpportunity/list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/copilotOpportunity/list.js b/src/routes/copilotOpportunity/list.js index 59fd5dc1..2cc4c152 100644 --- a/src/routes/copilotOpportunity/list.js +++ b/src/routes/copilotOpportunity/list.js @@ -35,7 +35,7 @@ module.exports = [ }, ], order: [ - [models.Sequelize.literal(`CASE WHEN status = 'active' THEN 0 ELSE 1 END`), 'ASC'], + [models.Sequelize.literal(`CASE WHEN "CopilotOpportunity"."status" = 'active' THEN 0 ELSE 1 END`), 'ASC'], [sortParams[0], sortParams[1]] ], limit, From c7578c59a1a7f3ca1a8798911b9c4003710ec7f3 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 18 Jul 2025 20:56:45 +0200 Subject: [PATCH 004/105] debug --- src/routes/copilotRequest/create.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/copilotRequest/create.js b/src/routes/copilotRequest/create.js index 2b05f524..11ec2c25 100644 --- a/src/routes/copilotRequest/create.js +++ b/src/routes/copilotRequest/create.js @@ -77,6 +77,7 @@ module.exports = [ }, }, }).then((copilotRequest) => { + req.log.info(copilotRequest, 'copilotRequest') if (copilotRequest && copilotRequest.data.projectType === data.data.projectType) { const err = new Error('There\'s a request of same type already!'); _.assign(err, { From 9783ab11af8befe461873f4e7731dafe1aa643a4 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 18 Jul 2025 21:57:05 +0200 Subject: [PATCH 005/105] debug --- src/routes/copilotRequest/approveRequest.service.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index fc0663d5..d291927e 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -5,6 +5,7 @@ import models from '../../models'; import { CONNECT_NOTIFICATION_EVENT, COPILOT_REQUEST_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants'; import util from '../../util'; import { createEvent } from '../../services/busApi'; +import { Op } from 'sequelize'; const resolveTransaction = (transaction, callback) => { if (transaction) { @@ -42,9 +43,13 @@ module.exports = (req, data, existingTransaction) => { where: { projectId, type: data.type, + status: { + [Op.notIn]: [COPILOT_REQUEST_STATUS.CANCELED], + } }, }) .then((existingCopilotOpportunityOfSameType) => { + req.log.debug(existingCopilotOpportunityOfSameType, 'askdjlasd'); if (existingCopilotOpportunityOfSameType) { const err = new Error('There\'s an opportunity of same type already!'); _.assign(err, { From 85bc8dabd171e4b9c550759baa8c2593a04508e3 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 18 Jul 2025 22:18:16 +0200 Subject: [PATCH 006/105] fix: allow creating copilot request if a request is canceled --- src/routes/copilotRequest/approveRequest.service.js | 1 - src/routes/copilotRequest/create.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index d291927e..a5c3b854 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -49,7 +49,6 @@ module.exports = (req, data, existingTransaction) => { }, }) .then((existingCopilotOpportunityOfSameType) => { - req.log.debug(existingCopilotOpportunityOfSameType, 'askdjlasd'); if (existingCopilotOpportunityOfSameType) { const err = new Error('There\'s an opportunity of same type already!'); _.assign(err, { diff --git a/src/routes/copilotRequest/create.js b/src/routes/copilotRequest/create.js index 11ec2c25..2b05f524 100644 --- a/src/routes/copilotRequest/create.js +++ b/src/routes/copilotRequest/create.js @@ -77,7 +77,6 @@ module.exports = [ }, }, }).then((copilotRequest) => { - req.log.info(copilotRequest, 'copilotRequest') if (copilotRequest && copilotRequest.data.projectType === data.data.projectType) { const err = new Error('There\'s a request of same type already!'); _.assign(err, { From 4895bce2e2caa4f347c14747d5cf54e1032fa36a Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 22 Jul 2025 00:03:23 +0300 Subject: [PATCH 007/105] PM-1498 - support for opportunity title --- src/routes/copilotRequest/create.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/copilotRequest/create.js b/src/routes/copilotRequest/create.js index 2b05f524..1522fac6 100644 --- a/src/routes/copilotRequest/create.js +++ b/src/routes/copilotRequest/create.js @@ -14,6 +14,7 @@ const addCopilotRequestValidations = { data: Joi.object() .keys({ projectId: Joi.number().required(), + opportunityTitle: Joi.string().required(), copilotUsername: Joi.string(), complexity: Joi.string().valid('low', 'medium', 'high').required(), requiresCommunication: Joi.string().valid('yes', 'no').required(), From 473b060d31c2d5dd9b82c4a3311d7e797f6b8b97 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 21 Jul 2025 23:48:35 +0200 Subject: [PATCH 008/105] fix: add extra info copilots email --- .../copilotRequest/approveRequest.service.js | 28 +++++++++++++++++++ src/utils/copilot.js | 16 +++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/utils/copilot.js diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index a5c3b854..7bd31715 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -6,6 +6,7 @@ import { CONNECT_NOTIFICATION_EVENT, COPILOT_REQUEST_STATUS, TEMPLATE_IDS, USER_ import util from '../../util'; import { createEvent } from '../../services/busApi'; import { Op } from 'sequelize'; +import { getCopilotTypeLabel } from '../../utils/copilot'; const resolveTransaction = (transaction, callback) => { if (transaction) { @@ -60,6 +61,30 @@ module.exports = (req, data, existingTransaction) => { .create(data, { transaction }); })) .then(async (opportunity) => { + const opportunityWithProjectInfo = await models.CopilotOpportunity.findOne({ + where: { id: opportunity.id }, + include: [ + { + model: models.CopilotRequest, + as: 'copilotRequest', + }, + { + model: models.Project, + as: 'project', + attributes: ['name'], + include: [ + { + model: models.ProjectMember, + as: 'members', + attributes: ['id', 'userId', 'role'], + }, + ], + }, + ], + }); + req.log.debug(opportunityWithProjectInfo, "debug log opportunityWithProjectInfo"); + const data = opportunityWithProjectInfo.copilotRequest.data; + req.log.debug(data, "debug log data"); const roles = await util.getRolesByRoleName(USER_ROLE.TC_COPILOT, req.log, req.id); const { subjects = [] } = await util.getRoleInfo(roles[0], req.log, req.id); const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL; @@ -71,6 +96,9 @@ module.exports = (req, data, existingTransaction) => { user_name: subject.handle, opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`, work_manager_url: config.get('workManagerUrl'), + opportunity_type: getCopilotTypeLabel(opportunity.type), + opportunity_title: opportunityWithProjectInfo.project.name, + start_date: moment.utc(data.startDate).format(), }, sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST, recipients: [subject.email], diff --git a/src/utils/copilot.js b/src/utils/copilot.js new file mode 100644 index 00000000..b7a722b7 --- /dev/null +++ b/src/utils/copilot.js @@ -0,0 +1,16 @@ +import { COPILOT_OPPORTUNITY_TYPE } from "../constants"; + +export const getCopilotTypeLabel = (type) => { + switch (type) { + case COPILOT_OPPORTUNITY_TYPE.AI: + return 'AI'; + case COPILOT_OPPORTUNITY_TYPE.DATA_SCIENCE: + return "Data Science"; + case COPILOT_OPPORTUNITY_TYPE.DESIGN: + return "Design"; + case COPILOT_OPPORTUNITY_TYPE.DEV: + return "Development"; + default: + return "Quality Assurance"; + } +}; From b63b2ade38a4e81a7cf73d6251d1586877ccfa9f Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 21 Jul 2025 23:49:01 +0200 Subject: [PATCH 009/105] fix: add extra info copilots email --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 83db4e29..1c2bc8c2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'pm-1368'] + only: ['develop', 'migration-setup', 'pm-1494'] - deployProd: context : org-global filters: From 191cc9803578deee6bff680b0b3874edd3980440 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 22 Jul 2025 13:44:00 +0300 Subject: [PATCH 010/105] PM-1499 - edit copilot request --- src/routes/copilotRequest/update.js | 79 +++++++++++++++++++++++++++++ src/routes/index.js | 3 +- 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/routes/copilotRequest/update.js diff --git a/src/routes/copilotRequest/update.js b/src/routes/copilotRequest/update.js new file mode 100644 index 00000000..bf141885 --- /dev/null +++ b/src/routes/copilotRequest/update.js @@ -0,0 +1,79 @@ +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; + +import models from '../../models'; +import util from '../../util'; +import { COPILOT_OPPORTUNITY_TYPE } from '../../constants'; +import { PERMISSION } from '../../permissions/constants'; + +const updateCopilotRequestValidations = { + body: Joi.object().keys({ + data: Joi.object() + .keys({ + projectId: Joi.number().required(), + copilotUsername: Joi.string(), + complexity: Joi.string().valid('low', 'medium', 'high'), + requiresCommunication: Joi.string().valid('yes', 'no'), + paymentType: Joi.string().valid('standard', 'other'), + otherPaymentType: Joi.string(), + opportunityTitle: Joi.string(), + projectType: Joi.string().valid(_.values(COPILOT_OPPORTUNITY_TYPE)), + overview: Joi.string().min(10), + skills: Joi.array().items( + Joi.object({ + id: Joi.string().required(), + name: Joi.string().required(), + }), + ), + startDate: Joi.date().iso(), + numWeeks: Joi.number().integer().positive(), + tzRestrictions: Joi.string(), + numHoursPerWeek: Joi.number().integer().positive(), + }) + .required(), + }), +}; + +module.exports = [ + validate(updateCopilotRequestValidations), + async (req, res, next) => { + const copilotRequestId = _.parseInt(req.params.copilotRequestId); + const patchData = req.body.data; + + if (!util.hasPermissionByReq(PERMISSION.MANAGE_COPILOT_REQUEST, req)) { + const err = new Error('Unable to update copilot request'); + _.assign(err, { + details: JSON.stringify({ message: 'You do not have permission to update copilot request' }), + status: 403, + }); + util.handleError('Permission error', err, req, next); + return; + } + + try { + const copilotRequest = await models.CopilotRequest.findOne({ + where: { id: copilotRequestId }, + }); + + if (!copilotRequest) { + const err = new Error(`Copilot request not found for id ${copilotRequestId}`); + err.status = 404; + throw err; + } + + // Only update fields provided in patchData + await copilotRequest.update(_.extend({ + data: patchData, + updatedBy: req.authUser.userId, + })); + + res.status(200).json(copilotRequest); + } catch (err) { + if (err.message) { + _.assign(err, { details: err.message }); + } + util.handleError('Error updating copilot request', err, req, next); + } + }, +]; diff --git a/src/routes/index.js b/src/routes/index.js index fd069fcd..87e48a86 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -396,7 +396,8 @@ router.route('/v5/projects/:projectId(\\d+)/settings') router.route('/v5/projects/copilots/requests') .get(require('./copilotRequest/list')); router.route('/v5/projects/copilots/requests/:copilotRequestId(\\d+)') - .get(require('./copilotRequest/get')); + .get(require('./copilotRequest/get')) + .patch(require('./copilotRequest/update')); router.route('/v5/projects/:projectId(\\d+)/copilots/requests') .get(require('./copilotRequest/list')) .post(require('./copilotRequest/create')); From 1a8d9f2e1f8a205014fce868759f86863b075d44 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 22 Jul 2025 17:24:54 +0200 Subject: [PATCH 011/105] fix: add extra info copilots email --- src/routes/copilotRequest/approveRequest.service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index 7bd31715..26488d5f 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -82,7 +82,8 @@ module.exports = (req, data, existingTransaction) => { }, ], }); - req.log.debug(opportunityWithProjectInfo, "debug log opportunityWithProjectInfo"); + req.log.debug(opportunityWithProjectInfo); + req.log.debug("debug log opportunityWithProjectInfo") const data = opportunityWithProjectInfo.copilotRequest.data; req.log.debug(data, "debug log data"); const roles = await util.getRolesByRoleName(USER_ROLE.TC_COPILOT, req.log, req.id); From e6f359597f3a020e096fbfbb252428681a689d6f Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 22 Jul 2025 17:53:04 +0200 Subject: [PATCH 012/105] fix: add extra info copilots email --- src/routes/copilotRequest/approveRequest.service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index 26488d5f..e8a3c52f 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -82,7 +82,8 @@ module.exports = (req, data, existingTransaction) => { }, ], }); - req.log.debug(opportunityWithProjectInfo); + req.log.info(opportunity); + req.log.info(opportunityWithProjectInfo); req.log.debug("debug log opportunityWithProjectInfo") const data = opportunityWithProjectInfo.copilotRequest.data; req.log.debug(data, "debug log data"); From f63c5808a0eaea3dfbc2b8d6ce04c3699c8aee91 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 22 Jul 2025 20:05:42 +0200 Subject: [PATCH 013/105] fix: add extra info copilots email --- .../copilotRequest/approveRequest.service.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index e8a3c52f..f960b592 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -61,13 +61,9 @@ module.exports = (req, data, existingTransaction) => { .create(data, { transaction }); })) .then(async (opportunity) => { - const opportunityWithProjectInfo = await models.CopilotOpportunity.findOne({ - where: { id: opportunity.id }, + const copilotRequestWithProjectInfo = await models.CopilotRequest.findOne({ + where: { id: opportunity.copilotRequestId }, include: [ - { - model: models.CopilotRequest, - as: 'copilotRequest', - }, { model: models.Project, as: 'project', @@ -82,10 +78,9 @@ module.exports = (req, data, existingTransaction) => { }, ], }); - req.log.info(opportunity); - req.log.info(opportunityWithProjectInfo); - req.log.debug("debug log opportunityWithProjectInfo") - const data = opportunityWithProjectInfo.copilotRequest.data; + req.log.info(copilotRequestWithProjectInfo); + req.log.debug("debug log copilotRequestWithProjectInfo") + const data = copilotRequestWithProjectInfo.data; req.log.debug(data, "debug log data"); const roles = await util.getRolesByRoleName(USER_ROLE.TC_COPILOT, req.log, req.id); const { subjects = [] } = await util.getRoleInfo(roles[0], req.log, req.id); @@ -99,7 +94,7 @@ module.exports = (req, data, existingTransaction) => { opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`, work_manager_url: config.get('workManagerUrl'), opportunity_type: getCopilotTypeLabel(opportunity.type), - opportunity_title: opportunityWithProjectInfo.project.name, + opportunity_title: copilotRequestWithProjectInfo.project.name, start_date: moment.utc(data.startDate).format(), }, sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST, From 2f46d059f9c245cc6f1dd1ee7d1eaab18545fccc Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 22 Jul 2025 20:29:38 +0200 Subject: [PATCH 014/105] fix: add extra info copilots email --- .../copilotRequest/approveRequest.service.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index f960b592..a6395bcc 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -63,20 +63,6 @@ module.exports = (req, data, existingTransaction) => { .then(async (opportunity) => { const copilotRequestWithProjectInfo = await models.CopilotRequest.findOne({ where: { id: opportunity.copilotRequestId }, - include: [ - { - model: models.Project, - as: 'project', - attributes: ['name'], - include: [ - { - model: models.ProjectMember, - as: 'members', - attributes: ['id', 'userId', 'role'], - }, - ], - }, - ], }); req.log.info(copilotRequestWithProjectInfo); req.log.debug("debug log copilotRequestWithProjectInfo") @@ -94,7 +80,7 @@ module.exports = (req, data, existingTransaction) => { opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`, work_manager_url: config.get('workManagerUrl'), opportunity_type: getCopilotTypeLabel(opportunity.type), - opportunity_title: copilotRequestWithProjectInfo.project.name, + opportunity_title: data.opportunityTitle, start_date: moment.utc(data.startDate).format(), }, sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST, From 2201ec4d107ba295898302cccf49e9b283001033 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 22 Jul 2025 21:32:47 +0200 Subject: [PATCH 015/105] fix: add extra info copilots email --- src/routes/copilotRequest/approveRequest.service.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index a6395bcc..c8c8473a 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -65,6 +65,7 @@ module.exports = (req, data, existingTransaction) => { where: { id: opportunity.copilotRequestId }, }); req.log.info(copilotRequestWithProjectInfo); + req.log.info(opportunity.copilotRequestId); req.log.debug("debug log copilotRequestWithProjectInfo") const data = copilotRequestWithProjectInfo.data; req.log.debug(data, "debug log data"); From cf50f644e86fd363785131ab2029d0e058d7a33c Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 22 Jul 2025 22:00:43 +0200 Subject: [PATCH 016/105] updated from develop --- .../copilotRequest/approveRequest.service.js | 14 +++----------- src/routes/copilotRequest/create.js | 1 + 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index c8c8473a..113100e3 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -17,7 +17,7 @@ const resolveTransaction = (transaction, callback) => { }; module.exports = (req, data, existingTransaction) => { - const { projectId, copilotRequestId } = data; + const { projectId, copilotRequestId, opportunityTitle, type } = data; return resolveTransaction(existingTransaction, transaction => models.Project.findOne({ @@ -61,14 +61,6 @@ module.exports = (req, data, existingTransaction) => { .create(data, { transaction }); })) .then(async (opportunity) => { - const copilotRequestWithProjectInfo = await models.CopilotRequest.findOne({ - where: { id: opportunity.copilotRequestId }, - }); - req.log.info(copilotRequestWithProjectInfo); - req.log.info(opportunity.copilotRequestId); - req.log.debug("debug log copilotRequestWithProjectInfo") - const data = copilotRequestWithProjectInfo.data; - req.log.debug(data, "debug log data"); const roles = await util.getRolesByRoleName(USER_ROLE.TC_COPILOT, req.log, req.id); const { subjects = [] } = await util.getRoleInfo(roles[0], req.log, req.id); const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL; @@ -80,8 +72,8 @@ module.exports = (req, data, existingTransaction) => { user_name: subject.handle, opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`, work_manager_url: config.get('workManagerUrl'), - opportunity_type: getCopilotTypeLabel(opportunity.type), - opportunity_title: data.opportunityTitle, + opportunity_type: getCopilotTypeLabel(type), + opportunity_title: opportunityTitle, start_date: moment.utc(data.startDate).format(), }, sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST, diff --git a/src/routes/copilotRequest/create.js b/src/routes/copilotRequest/create.js index 1522fac6..3bbd9376 100644 --- a/src/routes/copilotRequest/create.js +++ b/src/routes/copilotRequest/create.js @@ -98,6 +98,7 @@ module.exports = [ createdBy: req.authUser.userId, updatedBy: req.authUser.userId, type: copilotRequest.data.projectType, + opportunityTitle: copilotRequest.data.opportunityTitle, }); return approveRequest(req, approveData, transaction).then(() => copilotRequest); }).then(copilotRequest => res.status(201).json(copilotRequest)) From 56eb2b6e11967797d79b9eb4ef00b9aedd960b6a Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 22 Jul 2025 22:18:59 +0200 Subject: [PATCH 017/105] updated from develop --- src/routes/copilotRequest/approveRequest.service.js | 4 ++-- src/routes/copilotRequest/create.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index 113100e3..6da0fefd 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -17,7 +17,7 @@ const resolveTransaction = (transaction, callback) => { }; module.exports = (req, data, existingTransaction) => { - const { projectId, copilotRequestId, opportunityTitle, type } = data; + const { projectId, copilotRequestId, opportunityTitle, type, startDate } = data; return resolveTransaction(existingTransaction, transaction => models.Project.findOne({ @@ -74,7 +74,7 @@ module.exports = (req, data, existingTransaction) => { work_manager_url: config.get('workManagerUrl'), opportunity_type: getCopilotTypeLabel(type), opportunity_title: opportunityTitle, - start_date: moment.utc(data.startDate).format(), + start_date: moment.utc(startDate).format(), }, sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST, recipients: [subject.email], diff --git a/src/routes/copilotRequest/create.js b/src/routes/copilotRequest/create.js index 3bbd9376..52b640ea 100644 --- a/src/routes/copilotRequest/create.js +++ b/src/routes/copilotRequest/create.js @@ -99,6 +99,7 @@ module.exports = [ updatedBy: req.authUser.userId, type: copilotRequest.data.projectType, opportunityTitle: copilotRequest.data.opportunityTitle, + startDate: copilotRequest.data.startDate, }); return approveRequest(req, approveData, transaction).then(() => copilotRequest); }).then(copilotRequest => res.status(201).json(copilotRequest)) From fed04b85a1489c1be1a4881d41de004cfe808b3e Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 22 Jul 2025 23:28:40 +0200 Subject: [PATCH 018/105] fix: type --- src/routes/copilotRequest/approveRequest.service.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index 6da0fefd..3167c7c9 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -1,5 +1,6 @@ import _ from 'lodash'; import config from 'config'; +import moment from 'moment'; import models from '../../models'; import { CONNECT_NOTIFICATION_EVENT, COPILOT_REQUEST_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants'; From 64091d535dacccef6f55ff96cf094186b7a3f03e Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 22 Jul 2025 23:32:02 +0200 Subject: [PATCH 019/105] fix: type --- src/routes/copilotRequest/approveRequest.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index 3167c7c9..ef735ad0 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -1,12 +1,12 @@ import _ from 'lodash'; import config from 'config'; import moment from 'moment'; +import { Op } from 'sequelize'; import models from '../../models'; import { CONNECT_NOTIFICATION_EVENT, COPILOT_REQUEST_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants'; import util from '../../util'; import { createEvent } from '../../services/busApi'; -import { Op } from 'sequelize'; import { getCopilotTypeLabel } from '../../utils/copilot'; const resolveTransaction = (transaction, callback) => { From eaf21166a21bdfa69799872e11b091775f556b8b Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Jul 2025 00:12:40 +0200 Subject: [PATCH 020/105] fix: date format --- src/routes/copilotRequest/approveRequest.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index ef735ad0..32167a83 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -75,7 +75,7 @@ module.exports = (req, data, existingTransaction) => { work_manager_url: config.get('workManagerUrl'), opportunity_type: getCopilotTypeLabel(type), opportunity_title: opportunityTitle, - start_date: moment.utc(startDate).format(), + start_date: moment.utc(startDate).format("DD-MM-YYYY h:mm:ss a"), }, sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST, recipients: [subject.email], From 5bf08bede15d889f6e52662bac080267762b81b3 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Jul 2025 00:34:02 +0200 Subject: [PATCH 021/105] fix: date format --- src/routes/copilotRequest/approveRequest.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index 32167a83..37f7d25c 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -75,7 +75,7 @@ module.exports = (req, data, existingTransaction) => { work_manager_url: config.get('workManagerUrl'), opportunity_type: getCopilotTypeLabel(type), opportunity_title: opportunityTitle, - start_date: moment.utc(startDate).format("DD-MM-YYYY h:mm:ss a"), + start_date: moment.utc(startDate).format("YYYY-MM-DD HH:mm:ss [UTC]"), }, sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST, recipients: [subject.email], From 65e943eb8c0c3b990922da66c634a4d886ab4a62 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 23 Jul 2025 08:36:58 +0300 Subject: [PATCH 022/105] PM-1187 - notify copilot opportunities via slack --- config/custom-environment-variables.json | 1 + config/default.json | 1 + config/production.json | 3 ++- .../copilotRequest/approveRequest.service.js | 15 +++++++++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 2d1d9475..4a56c8a5 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -53,6 +53,7 @@ "AUTH0_PROXY_SERVER_URL" : "AUTH0_PROXY_SERVER_URL", "connectUrl": "CONNECT_URL", "workManagerUrl": "WORK_MANAGER_URL", + "copilotsSlackEmail": "COPILOTS_SLACK_EMAIL", "accountsAppUrl": "ACCOUNTS_APP_URL", "inviteEmailSubject": "INVITE_EMAIL_SUBJECT", "inviteEmailSectionTitle": "INVITE_EMAIL_SECTION_TITLE", diff --git a/config/default.json b/config/default.json index d65b6383..ebdea119 100644 --- a/config/default.json +++ b/config/default.json @@ -56,6 +56,7 @@ "workManagerUrl": "https://challenges.topcoder-dev.com", "copilotPortalUrl": "https://copilots.topcoder-dev.com", "accountsAppUrl": "https://accounts.topcoder-dev.com", + "copilotsSlackEmail": "aaaaplg3e4y6ccn3qniivvnxva@topcoder.slack.com", "MAX_REVISION_NUMBER": 100, "UNIQUE_GMAIL_VALIDATION": false, "pageSize": 20, diff --git a/config/production.json b/config/production.json index 73399edf..67418d53 100644 --- a/config/production.json +++ b/config/production.json @@ -4,5 +4,6 @@ "copilotPortalUrl": "https://copilots.topcoder.com", "sfdcBillingAccountNameField": "Billing_Account_name__c", "sfdcBillingAccountMarkupField": "Mark_up__c", - "sfdcBillingAccountActiveField": "Active__c" + "sfdcBillingAccountActiveField": "Active__c", + "copilotsSlackEmail": "mem-copilots-aaaaaa4m4blyjxrlrv7yr37y6u@topcoder.slack.com" } diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index 37f7d25c..7422ce84 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -83,6 +83,21 @@ module.exports = (req, data, existingTransaction) => { }, req.log); }); + // send email to notify via slack + createEvent(emailEventType, { + data: { + user_name: 'Copilots', + opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`, + work_manager_url: config.get('workManagerUrl'), + opportunity_type: getCopilotTypeLabel(type), + opportunity_title: opportunityTitle, + start_date: moment.utc(startDate).format("YYYY-MM-DD HH:mm:ss [UTC]"), + }, + sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST, + recipients: [config.copilotsSlackEmail], + version: 'v3', + }, req.log); + req.log.info("Finished sending emails to copilots"); return opportunity; From 624cad57e42dc539a59ae53fbd0660657853b289 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 23 Jul 2025 08:41:17 +0300 Subject: [PATCH 023/105] Move to reusable function --- .../copilotRequest/approveRequest.service.js | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index 7422ce84..faf0149c 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -67,26 +67,10 @@ module.exports = (req, data, existingTransaction) => { const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL; const copilotPortalUrl = config.get('copilotPortalUrl'); req.log.info("Sending emails to all copilots about new opportunity"); - subjects.forEach(subject => { - createEvent(emailEventType, { - data: { - user_name: subject.handle, - opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`, - work_manager_url: config.get('workManagerUrl'), - opportunity_type: getCopilotTypeLabel(type), - opportunity_title: opportunityTitle, - start_date: moment.utc(startDate).format("YYYY-MM-DD HH:mm:ss [UTC]"), - }, - sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST, - recipients: [subject.email], - version: 'v3', - }, req.log); - }); - // send email to notify via slack - createEvent(emailEventType, { + const sendNotification = (userName, recipients) => createEvent(emailEventType, { data: { - user_name: 'Copilots', + user_name: userName, opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`, work_manager_url: config.get('workManagerUrl'), opportunity_type: getCopilotTypeLabel(type), @@ -94,10 +78,15 @@ module.exports = (req, data, existingTransaction) => { start_date: moment.utc(startDate).format("YYYY-MM-DD HH:mm:ss [UTC]"), }, sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST, - recipients: [config.copilotsSlackEmail], + recipients, version: 'v3', }, req.log); + subjects.forEach(subject => sendNotification(subject.handle, subject.email)); + + // send email to notify via slack + sendNotification('Copilots', [config.copilotsSlackEmail]); + req.log.info("Finished sending emails to copilots"); return opportunity; From f79f42bfef7efa946a943e729ba4072d553cff0e Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 23 Jul 2025 10:48:13 +0300 Subject: [PATCH 024/105] fix recipients --- src/routes/copilotRequest/approveRequest.service.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index faf0149c..d1fa2bf2 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -68,7 +68,7 @@ module.exports = (req, data, existingTransaction) => { const copilotPortalUrl = config.get('copilotPortalUrl'); req.log.info("Sending emails to all copilots about new opportunity"); - const sendNotification = (userName, recipients) => createEvent(emailEventType, { + const sendNotification = (userName, recipient) => createEvent(emailEventType, { data: { user_name: userName, opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`, @@ -78,14 +78,14 @@ module.exports = (req, data, existingTransaction) => { start_date: moment.utc(startDate).format("YYYY-MM-DD HH:mm:ss [UTC]"), }, sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST, - recipients, + recipients: [recipient], version: 'v3', }, req.log); subjects.forEach(subject => sendNotification(subject.handle, subject.email)); // send email to notify via slack - sendNotification('Copilots', [config.copilotsSlackEmail]); + sendNotification('Copilots', config.copilotsSlackEmail); req.log.info("Finished sending emails to copilots"); From 8a823f2827b2cdd6414a6caa602cbea6c1a234ac Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Jul 2025 16:34:32 +0200 Subject: [PATCH 025/105] fix: date format --- src/routes/copilotRequest/approveRequest.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index d1fa2bf2..31e8db89 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -75,7 +75,7 @@ module.exports = (req, data, existingTransaction) => { work_manager_url: config.get('workManagerUrl'), opportunity_type: getCopilotTypeLabel(type), opportunity_title: opportunityTitle, - start_date: moment.utc(startDate).format("YYYY-MM-DD HH:mm:ss [UTC]"), + start_date: moment(startDate).format("DD-MM-YYYY"), }, sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST, recipients: [recipient], From 7b853be429f1611874691e56ebe022662dbfafdd Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Jul 2025 16:34:47 +0200 Subject: [PATCH 026/105] fix: date format --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1c2bc8c2..d42ad016 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'pm-1494'] + only: ['develop', 'migration-setup', 'pm-1494_1'] - deployProd: context : org-global filters: From 9c9edce43fb463f0d32d995c50519333f0c16e06 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Jul 2025 17:18:29 +0200 Subject: [PATCH 027/105] fix: add opportunity title and type in PM emails --- src/routes/copilotOpportunityApply/create.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index 5c2f8346..6c7bedfc 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -41,6 +41,12 @@ module.exports = [ where: { id: copilotOpportunityId, }, + include: [ + { + model: models.CopilotRequest, + as: 'copilotRequest', + }, + ], }).then(async (opportunity) => { if (!opportunity) { const err = new Error('No opportunity found'); @@ -92,12 +98,15 @@ module.exports = [ const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL; const copilotPortalUrl = config.get('copilotPortalUrl'); + const requestData = opportunity.copilotRequest.data; listOfSubjects.forEach((subject) => { createEvent(emailEventType, { data: { user_name: subject.handle, opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}#applications`, work_manager_url: config.get('workManagerUrl'), + opportunity_type: getCopilotTypeLabel(requestData.projectType), + opportunity_title: requestData.opportunityTitle, }, sendgrid_template_id: TEMPLATE_IDS.APPLY_COPILOT, recipients: [subject.email], From 97becbbccbeafe1817c89176135513aa22af24f2 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Jul 2025 17:18:46 +0200 Subject: [PATCH 028/105] fix: add opportunity title and type in PM emails --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d42ad016..a0945e3b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'pm-1494_1'] + only: ['develop', 'migration-setup', 'pm-1497'] - deployProd: context : org-global filters: From cc15920767a00a0ac9c7d7c88b583c220754298c Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Jul 2025 17:26:13 +0200 Subject: [PATCH 029/105] fix: add opportunity title and type in PM emails --- src/routes/copilotOpportunityApply/create.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index 6c7bedfc..2364e034 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -8,6 +8,7 @@ import util from '../../util'; import { PERMISSION } from '../../permissions/constants'; import { CONNECT_NOTIFICATION_EVENT, COPILOT_OPPORTUNITY_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants'; import { createEvent } from '../../services/busApi'; +import { getCopilotTypeLabel } from '../../utils/copilot'; const applyCopilotRequestValidations = { body: Joi.object().keys({ From 25b8b346d5306115ad76b33b7430dd4159bc342d Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Jul 2025 21:54:09 +0200 Subject: [PATCH 030/105] fix: removed logic to allow inviting user even if they are already a member --- src/routes/projectMemberInvites/create.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 99f4334a..47839596 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -322,18 +322,6 @@ module.exports = [ const errorMessageForAlreadyMemberUser = 'User with such handle is already a member of the team.'; if (inviteUserIds) { - // remove members already in the team - _.remove(inviteUserIds, u => _.some(members, (m) => { - const isPresent = m.userId === u; - if (isPresent) { - failed.push(_.assign({}, { - handle: getUserHandleById(m.userId, inviteUsers), - message: errorMessageForAlreadyMemberUser, - })); - } - return isPresent; - })); - // for each user invited by `handle` (userId) we have to load they Topcoder Roles, // so we can check if such a user can be invited with desired Project Role // for customers we don't check it to avoid extra call, as any Topcoder user can be invited as customer From 7831f541b669844e656fcf5a0b30a28e74f8616f Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Jul 2025 21:56:22 +0200 Subject: [PATCH 031/105] fix: allow copilots to be added even if the existing member --- src/routes/projectMemberInvites/create.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 47839596..13a3cd84 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -322,6 +322,20 @@ module.exports = [ const errorMessageForAlreadyMemberUser = 'User with such handle is already a member of the team.'; if (inviteUserIds) { + if (invite.role !== PROJECT_MEMBER_ROLE.COPILOT) { + // remove members already in the team + _.remove(inviteUserIds, u => _.some(members, (m) => { + const isPresent = m.userId === u; + if (isPresent) { + failed.push(_.assign({}, { + handle: getUserHandleById(m.userId, inviteUsers), + message: errorMessageForAlreadyMemberUser, + })); + } + return isPresent; + })); + } + // for each user invited by `handle` (userId) we have to load they Topcoder Roles, // so we can check if such a user can be invited with desired Project Role // for customers we don't check it to avoid extra call, as any Topcoder user can be invited as customer From 57a8d00db2801611d0eb9072bd6d3bd747f65b3b Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Jul 2025 22:16:23 +0200 Subject: [PATCH 032/105] fix: allow copilots to be added even if the existing member --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a0945e3b..e5ab511c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'pm-1497'] + only: ['develop', 'migration-setup', 'pm-1506'] - deployProd: context : org-global filters: From 1b21d3bf1cfbdf0aa094985c1132a9bff1e4e418 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Jul 2025 23:27:49 +0200 Subject: [PATCH 033/105] fix: allow copilots to be added even if the existing member --- src/util.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/util.js b/src/util.js index 60c71d57..2c735501 100644 --- a/src/util.js +++ b/src/util.js @@ -937,7 +937,19 @@ const projectServiceUtils = { // check if member is already registered const existingMember = _.find(members, m => m.userId === member.userId); - if (existingMember) { + // if (existingMember) { + // const err = new Error(`User already registered for role: ${existingMember.role}`); + // err.status = 400; + // return Promise.reject(err); + // } + + if (existingMember + && member.role === PROJECT_MEMBER_ROLE.COPILOT + && existingMember.role === PROJECT_MEMBER_ROLE.OBSERVER) { + yield models.ProjectMember + .update({ deletedBy: req.authUser.userId }) + .then(entity => entity.destroy()); + } else if (existingMember) { const err = new Error(`User already registered for role: ${existingMember.role}`); err.status = 400; return Promise.reject(err); From fa133301cd0f87d1cea3f225c78d1a8ab992a0c5 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Jul 2025 23:53:33 +0200 Subject: [PATCH 034/105] fix: allow copilots to be added even if the existing member --- src/util.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/util.js b/src/util.js index 2c735501..cc8cbdf0 100644 --- a/src/util.js +++ b/src/util.js @@ -947,7 +947,10 @@ const projectServiceUtils = { && member.role === PROJECT_MEMBER_ROLE.COPILOT && existingMember.role === PROJECT_MEMBER_ROLE.OBSERVER) { yield models.ProjectMember - .update({ deletedBy: req.authUser.userId }) + .update( + { deletedBy: req.authUser.userId }, + { where: { userId: member.userId }, transaction} + ) .then(entity => entity.destroy()); } else if (existingMember) { const err = new Error(`User already registered for role: ${existingMember.role}`); From 05369108850a8f4a4022ea373e6bb466d1f3f315 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Jul 2025 00:20:51 +0200 Subject: [PATCH 035/105] fix: allow copilots to be added even if the existing member --- src/util.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/util.js b/src/util.js index cc8cbdf0..a1cf1d4d 100644 --- a/src/util.js +++ b/src/util.js @@ -950,8 +950,11 @@ const projectServiceUtils = { .update( { deletedBy: req.authUser.userId }, { where: { userId: member.userId }, transaction} - ) - .then(entity => entity.destroy()); + ); + yield models.ProjectMember.destroy({ + where: { userId: member.userId }, + transaction + }); } else if (existingMember) { const err = new Error(`User already registered for role: ${existingMember.role}`); err.status = 400; From b67a6ba26f0865f800e2c815e93b017568f923dd Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Jul 2025 00:43:18 +0200 Subject: [PATCH 036/105] fix: allow copilots to be added even if the existing member --- src/util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util.js b/src/util.js index a1cf1d4d..d4750f8b 100644 --- a/src/util.js +++ b/src/util.js @@ -949,10 +949,10 @@ const projectServiceUtils = { yield models.ProjectMember .update( { deletedBy: req.authUser.userId }, - { where: { userId: member.userId }, transaction} + { where: { userId: existingMember.userId }, transaction} ); yield models.ProjectMember.destroy({ - where: { userId: member.userId }, + where: { userId: existingMember.userId }, transaction }); } else if (existingMember) { From ff30126203868d53354a5c10cf8e315bd462e73b Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 24 Jul 2025 13:35:52 +0300 Subject: [PATCH 037/105] PM-1499 - allow PMs to fetch & edit any copilot requests --- src/routes/copilotRequest/get.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/routes/copilotRequest/get.js b/src/routes/copilotRequest/get.js index 6284b227..9102317a 100644 --- a/src/routes/copilotRequest/get.js +++ b/src/routes/copilotRequest/get.js @@ -1,7 +1,6 @@ import _ from 'lodash'; import models from '../../models'; -import { ADMIN_ROLES } from '../../constants'; import util from '../../util'; import { PERMISSION } from '../../permissions/constants'; @@ -16,16 +15,10 @@ module.exports = [ return next(err); } - const isAdmin = util.hasRoles(req, ADMIN_ROLES); - - const userId = req.authUser.userId; const copilotRequestId = _.parseInt(req.params.copilotRequestId); // Admin can see all requests and the PM can only see requests created by them - const whereCondition = _.assign({}, - isAdmin ? {} : { createdBy: userId }, - { id: copilotRequestId }, - ); + const whereCondition = { id: copilotRequestId }; return models.CopilotRequest.findOne({ where: whereCondition, From f38a2b69819596b35e6d21e775f75fa0e6b9fa84 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 24 Jul 2025 13:36:26 +0300 Subject: [PATCH 038/105] PM-1499 - when updating copilot request, check for same type request --- src/routes/copilotRequest/update.js | 31 ++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/routes/copilotRequest/update.js b/src/routes/copilotRequest/update.js index bf141885..7f9ab4ee 100644 --- a/src/routes/copilotRequest/update.js +++ b/src/routes/copilotRequest/update.js @@ -4,8 +4,9 @@ import Joi from 'joi'; import models from '../../models'; import util from '../../util'; -import { COPILOT_OPPORTUNITY_TYPE } from '../../constants'; +import { COPILOT_OPPORTUNITY_TYPE, COPILOT_REQUEST_STATUS } from '../../constants'; import { PERMISSION } from '../../permissions/constants'; +import { Op } from 'sequelize'; const updateCopilotRequestValidations = { body: Joi.object().keys({ @@ -62,9 +63,33 @@ module.exports = [ throw err; } + // check if same type of copilot request already exists + if (patchData.projectType !== undefined && patchData.projectType !== copilotRequest.data.projectType) { + const sameTypeRequest = await models.CopilotRequest.findOne({ + where: { + projectId: copilotRequest.projectId, + status: { + [Op.in]: [COPILOT_REQUEST_STATUS.NEW, COPILOT_REQUEST_STATUS.APPROVED, COPILOT_REQUEST_STATUS.SEEKING], + }, + data: { + projectType: patchData.projectType, + }, + id: { [Op.not]: copilotRequestId }, + }, + }); + + if (sameTypeRequest) { + const err = new Error('There\'s a request of same type already!'); + _.assign(err, { + status: 400, + }); + throw err; + } + } + // Only update fields provided in patchData - await copilotRequest.update(_.extend({ - data: patchData, + await copilotRequest.update(_.assign({ + data: _.assign(copilotRequest.data, patchData), updatedBy: req.authUser.userId, })); From f26de03f1ea32fdc3e69607c72c56f0f8c66368b Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Jul 2025 22:01:19 +0200 Subject: [PATCH 039/105] fix: just switch role if user is already a member --- src/routes/projectMemberInvites/create.js | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 13a3cd84..614ef673 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -334,6 +334,43 @@ module.exports = [ } return isPresent; })); + } else { + const existingMembers = _.filter(members, (m) => { + return inviteUserIds.includes(m.userId); + }); + + if (existingMembers.length > 0) { + const updatePromises = existingMembers.map(item => models.ProjectMember.update({ + role: invite.role, + updatedBy: req.authUser.userId, + }, { + where: { + userId: item.userId, + returning: true + }, + })); + return Promise.all(updatePromises).then((response) => { + const [, updatedRecord] = response; + return updatedRecord; + }).then(values => ( + // populate successful invites with user details if required + util.getObjectsWithMemberDetails(values, fields, req) + .catch((err) => { + req.log.error('Cannot get user details for invites.'); + req.log.debug('Error during getting user details for invites', err); + // continues without details anyway + return values; + }) + )) + .then((values) => { + const response = _.assign({}, { success: util.postProcessInvites('$[*]', values, req) }); + if (failed.length) { + res.status(403).json(_.assign({}, response, { failed })); + } else { + res.status(201).json(response); + } + }); + } } // for each user invited by `handle` (userId) we have to load they Topcoder Roles, From 50d2dac51f0b705d718169fe413625fe6617057f Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Jul 2025 22:27:46 +0200 Subject: [PATCH 040/105] fix: just switch role if user is already a member --- src/routes/projectMemberInvites/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 614ef673..45c9a379 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -346,8 +346,8 @@ module.exports = [ }, { where: { userId: item.userId, - returning: true }, + returning: true })); return Promise.all(updatePromises).then((response) => { const [, updatedRecord] = response; From 347caaec5cd51e5b25fe60be011f9c6b5343829c Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Jul 2025 23:06:18 +0200 Subject: [PATCH 041/105] fix: debug logs --- src/routes/projectMemberInvites/create.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 45c9a379..be46968f 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -339,6 +339,8 @@ module.exports = [ return inviteUserIds.includes(m.userId); }); + req.log.debug(`Existing members: ${JSON.stringify(existingMembers)}`); + if (existingMembers.length > 0) { const updatePromises = existingMembers.map(item => models.ProjectMember.update({ role: invite.role, @@ -351,6 +353,7 @@ module.exports = [ })); return Promise.all(updatePromises).then((response) => { const [, updatedRecord] = response; + req.log.debug(`Updated member: ${JSON.stringify(updatedRecord)}`); return updatedRecord; }).then(values => ( // populate successful invites with user details if required @@ -364,6 +367,7 @@ module.exports = [ )) .then((values) => { const response = _.assign({}, { success: util.postProcessInvites('$[*]', values, req) }); + req.log.debug(`Response: ${JSON.stringify(response)} ${JSON.stringify(values)}`); if (failed.length) { res.status(403).json(_.assign({}, response, { failed })); } else { From d4e4033b78ada76d788644e0afc6c053ff439e02 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Jul 2025 23:36:22 +0200 Subject: [PATCH 042/105] fix: debug logs --- src/routes/projectMemberInvites/create.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index be46968f..54179dad 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -18,6 +18,7 @@ import { } from '../../constants'; import { createEvent } from '../../services/busApi'; import { PERMISSION, PROJECT_TO_TOPCODER_ROLES_MATRIX } from '../../permissions/constants'; +import { Op } from 'sequelize'; const ALLOWED_FIELDS = _.keys(models.ProjectMemberInvite.rawAttributes).concat(['handle']); @@ -342,19 +343,26 @@ module.exports = [ req.log.debug(`Existing members: ${JSON.stringify(existingMembers)}`); if (existingMembers.length > 0) { - const updatePromises = existingMembers.map(item => models.ProjectMember.update({ + const existingMemberIds = existingMembers.map(item => item.userId); + const updatePromises = existingMemberIds.map(userId => models.ProjectMember.update({ role: invite.role, updatedBy: req.authUser.userId, }, { where: { - userId: item.userId, + userId, }, returning: true })); - return Promise.all(updatePromises).then((response) => { - const [, updatedRecord] = response; - req.log.debug(`Updated member: ${JSON.stringify(updatedRecord)}`); - return updatedRecord; + return Promise.all(updatePromises).then(async () => { + const updatedMembers = await models.ProjectMember.findAll({ + where: { + usedId: { + [Op.in]: existingMemberIds, + }, + }, + }); + req.log.debug(`Updated member: ${JSON.stringify(updatedMembers)}`); + return updatedMembers; }).then(values => ( // populate successful invites with user details if required util.getObjectsWithMemberDetails(values, fields, req) From ecb836c0807206d22efd3a81ea27a7e883a52503 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 25 Jul 2025 00:00:21 +0200 Subject: [PATCH 043/105] fix: debug logs --- src/routes/projectMemberInvites/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 54179dad..866c0d14 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -356,7 +356,7 @@ module.exports = [ return Promise.all(updatePromises).then(async () => { const updatedMembers = await models.ProjectMember.findAll({ where: { - usedId: { + userId: { [Op.in]: existingMemberIds, }, }, From 279d2f17a153b4dc7e10f5280d3e7a22efbd6981 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 25 Jul 2025 00:31:26 +0200 Subject: [PATCH 044/105] fix: update kafka --- src/routes/projectMemberInvites/create.js | 9 +++++++++ src/util.js | 20 +------------------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 866c0d14..1b0ac0dc 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -361,6 +361,15 @@ module.exports = [ }, }, }); + + updatedMembers.forEach((updatedMember) => { + util.sendResourceToKafkaBus( + req, + EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED, + RESOURCES.PROJECT_MEMBER, + updatedMember, + ); + }); req.log.debug(`Updated member: ${JSON.stringify(updatedMembers)}`); return updatedMembers; }).then(values => ( diff --git a/src/util.js b/src/util.js index d4750f8b..60c71d57 100644 --- a/src/util.js +++ b/src/util.js @@ -937,25 +937,7 @@ const projectServiceUtils = { // check if member is already registered const existingMember = _.find(members, m => m.userId === member.userId); - // if (existingMember) { - // const err = new Error(`User already registered for role: ${existingMember.role}`); - // err.status = 400; - // return Promise.reject(err); - // } - - if (existingMember - && member.role === PROJECT_MEMBER_ROLE.COPILOT - && existingMember.role === PROJECT_MEMBER_ROLE.OBSERVER) { - yield models.ProjectMember - .update( - { deletedBy: req.authUser.userId }, - { where: { userId: existingMember.userId }, transaction} - ); - yield models.ProjectMember.destroy({ - where: { userId: existingMember.userId }, - transaction - }); - } else if (existingMember) { + if (existingMember) { const err = new Error(`User already registered for role: ${existingMember.role}`); err.status = 400; return Promise.reject(err); From 3998114f5163481ab02c87c412297145503d7af5 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 25 Jul 2025 01:07:36 +0200 Subject: [PATCH 045/105] fix: update kafka --- src/routes/projectMemberInvites/create.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 1b0ac0dc..250eb250 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -300,7 +300,7 @@ module.exports = [ return []; }) - .then((inviteUsers) => { + .then(async (inviteUsers) => { const members = req.context.currentProjectMembers; const projectId = _.parseInt(req.params.projectId); // check user handle exists in returned result @@ -337,13 +337,25 @@ module.exports = [ })); } else { const existingMembers = _.filter(members, (m) => { - return inviteUserIds.includes(m.userId); + return inviteUserIds.includes(m.userId) && m.projectId === projectId; }); req.log.debug(`Existing members: ${JSON.stringify(existingMembers)}`); if (existingMembers.length > 0) { const existingMemberIds = existingMembers.map(item => item.userId); + const membersBeforeUpdate = await models.ProjectMember.findAll({ + where: { + userId: { + [Op.in]: existingMemberIds, + }, + }, + }); + const membersMap = membersBeforeUpdate.reduce((acc, member) => { + acc[member.id] = member; + return acc; + }, {}); + const updatePromises = existingMemberIds.map(userId => models.ProjectMember.update({ role: invite.role, updatedBy: req.authUser.userId, @@ -368,6 +380,7 @@ module.exports = [ EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED, RESOURCES.PROJECT_MEMBER, updatedMember, + membersMap[updatedMember.id] ); }); req.log.debug(`Updated member: ${JSON.stringify(updatedMembers)}`); From b7c5f95f5049d540e4d2f8ba44682313c2e24be6 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 25 Jul 2025 01:12:23 +0200 Subject: [PATCH 046/105] revert --- src/routes/projectMemberInvites/create.js | 89 ++--------------------- 1 file changed, 8 insertions(+), 81 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 250eb250..922e5ad4 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -323,89 +323,16 @@ module.exports = [ const errorMessageForAlreadyMemberUser = 'User with such handle is already a member of the team.'; if (inviteUserIds) { - if (invite.role !== PROJECT_MEMBER_ROLE.COPILOT) { - // remove members already in the team - _.remove(inviteUserIds, u => _.some(members, (m) => { - const isPresent = m.userId === u; - if (isPresent) { - failed.push(_.assign({}, { - handle: getUserHandleById(m.userId, inviteUsers), - message: errorMessageForAlreadyMemberUser, - })); - } - return isPresent; - })); - } else { - const existingMembers = _.filter(members, (m) => { - return inviteUserIds.includes(m.userId) && m.projectId === projectId; - }); - - req.log.debug(`Existing members: ${JSON.stringify(existingMembers)}`); - - if (existingMembers.length > 0) { - const existingMemberIds = existingMembers.map(item => item.userId); - const membersBeforeUpdate = await models.ProjectMember.findAll({ - where: { - userId: { - [Op.in]: existingMemberIds, - }, - }, - }); - const membersMap = membersBeforeUpdate.reduce((acc, member) => { - acc[member.id] = member; - return acc; - }, {}); - - const updatePromises = existingMemberIds.map(userId => models.ProjectMember.update({ - role: invite.role, - updatedBy: req.authUser.userId, - }, { - where: { - userId, - }, - returning: true + _.remove(inviteUserIds, u => _.some(members, (m) => { + const isPresent = m.userId === u; + if (isPresent) { + failed.push(_.assign({}, { + handle: getUserHandleById(m.userId, inviteUsers), + message: errorMessageForAlreadyMemberUser, })); - return Promise.all(updatePromises).then(async () => { - const updatedMembers = await models.ProjectMember.findAll({ - where: { - userId: { - [Op.in]: existingMemberIds, - }, - }, - }); - - updatedMembers.forEach((updatedMember) => { - util.sendResourceToKafkaBus( - req, - EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED, - RESOURCES.PROJECT_MEMBER, - updatedMember, - membersMap[updatedMember.id] - ); - }); - req.log.debug(`Updated member: ${JSON.stringify(updatedMembers)}`); - return updatedMembers; - }).then(values => ( - // populate successful invites with user details if required - util.getObjectsWithMemberDetails(values, fields, req) - .catch((err) => { - req.log.error('Cannot get user details for invites.'); - req.log.debug('Error during getting user details for invites', err); - // continues without details anyway - return values; - }) - )) - .then((values) => { - const response = _.assign({}, { success: util.postProcessInvites('$[*]', values, req) }); - req.log.debug(`Response: ${JSON.stringify(response)} ${JSON.stringify(values)}`); - if (failed.length) { - res.status(403).json(_.assign({}, response, { failed })); - } else { - res.status(201).json(response); - } - }); } - } + return isPresent; + })); // for each user invited by `handle` (userId) we have to load they Topcoder Roles, // so we can check if such a user can be invited with desired Project Role From bebe265ade684abcc560521fab6946e7a6e886cc Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 25 Jul 2025 01:31:18 +0200 Subject: [PATCH 047/105] fix: build --- src/routes/projectMemberInvites/create.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 922e5ad4..6e72b5ea 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -18,7 +18,6 @@ import { } from '../../constants'; import { createEvent } from '../../services/busApi'; import { PERMISSION, PROJECT_TO_TOPCODER_ROLES_MATRIX } from '../../permissions/constants'; -import { Op } from 'sequelize'; const ALLOWED_FIELDS = _.keys(models.ProjectMemberInvite.rawAttributes).concat(['handle']); @@ -300,7 +299,7 @@ module.exports = [ return []; }) - .then(async (inviteUsers) => { + .then((inviteUsers) => { const members = req.context.currentProjectMembers; const projectId = _.parseInt(req.params.projectId); // check user handle exists in returned result From b7c7f3453ad5ee78fbb5de65c64e9c279a8cdb83 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 25 Jul 2025 10:24:51 +0300 Subject: [PATCH 048/105] PM-1499 - update opportunity type & make sure canceled & fulfilled are not editable --- src/routes/copilotRequest/update.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/routes/copilotRequest/update.js b/src/routes/copilotRequest/update.js index 7f9ab4ee..712a590e 100644 --- a/src/routes/copilotRequest/update.js +++ b/src/routes/copilotRequest/update.js @@ -63,6 +63,12 @@ module.exports = [ throw err; } + if (['canceled', 'fulfilled'].includes(copilotRequest.status)) { + const err = new Error(`Copilot request with status ${copilotRequest.status} cannot be updated!`); + err.status = 400; + throw err; + } + // check if same type of copilot request already exists if (patchData.projectType !== undefined && patchData.projectType !== copilotRequest.data.projectType) { const sameTypeRequest = await models.CopilotRequest.findOne({ @@ -87,6 +93,11 @@ module.exports = [ } } + // if type changes, make sure we update "type" on opportunity as well + if (patchData.projectType) { + patchData.type = patchData.projectType; + } + // Only update fields provided in patchData await copilotRequest.update(_.assign({ data: _.assign(copilotRequest.data, patchData), From 9385156b92bcb748a91578c245d0f302f4e07cc0 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 25 Jul 2025 17:17:37 +0200 Subject: [PATCH 049/105] fix: added error string and already assigned role --- src/routes/projectMemberInvites/create.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 6e72b5ea..6b89de7f 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -328,6 +328,8 @@ module.exports = [ failed.push(_.assign({}, { handle: getUserHandleById(m.userId, inviteUsers), message: errorMessageForAlreadyMemberUser, + error: "ALREADY_MEMBER", + role: m.role, })); } return isPresent; From fdb09e3b18b9a28a9f8b277cb1ce86e13937df64 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 25 Jul 2025 20:01:38 +0200 Subject: [PATCH 050/105] fix: added error string and already assigned role --- src/routes/projectMemberInvites/create.js | 29 +++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 6b89de7f..896ed232 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -18,6 +18,7 @@ import { } from '../../constants'; import { createEvent } from '../../services/busApi'; import { PERMISSION, PROJECT_TO_TOPCODER_ROLES_MATRIX } from '../../permissions/constants'; +import { Op } from 'sequelize'; const ALLOWED_FIELDS = _.keys(models.ProjectMemberInvite.rawAttributes).concat(['handle']); @@ -299,7 +300,7 @@ module.exports = [ return []; }) - .then((inviteUsers) => { + .then(async (inviteUsers) => { const members = req.context.currentProjectMembers; const projectId = _.parseInt(req.params.projectId); // check user handle exists in returned result @@ -322,6 +323,30 @@ module.exports = [ const errorMessageForAlreadyMemberUser = 'User with such handle is already a member of the team.'; if (inviteUserIds) { + const existingMembers = _.some(members, (m) => { + const isPresent = m.userId === u; + return isPresent; + }); + + const projectMembers = await models.ProjectMember.findAll({ + where: { + userId: { + [Op.in]: existingMembers.map(item => item.userId), + } + } + }); + + req.log.debug(`Existing Project Members: ${JSON.stringify(projectMembers)}`); + + const existingProjectMembersMap = projectMembers.reduce((acc, current) => { + return { + ...acc, + [current.id]: current, + }; + }, {}); + + req.log.debug(`Existing Project Members Map: ${JSON.stringify(existingProjectMembersMap)}`); + _.remove(inviteUserIds, u => _.some(members, (m) => { const isPresent = m.userId === u; if (isPresent) { @@ -329,7 +354,7 @@ module.exports = [ handle: getUserHandleById(m.userId, inviteUsers), message: errorMessageForAlreadyMemberUser, error: "ALREADY_MEMBER", - role: m.role, + role: existingProjectMembersMap[m.userId].role, })); } return isPresent; From 2dc0ea28e333651a07bc8efcc8ed183ce68e4bd6 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 25 Jul 2025 20:29:17 +0200 Subject: [PATCH 051/105] fix: added error string and already assigned role --- src/routes/projectMembers/update.js | 78 +++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index 48ba6946..527f6915 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -5,8 +5,9 @@ import Joi from 'joi'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; -import { EVENT, RESOURCES, PROJECT_MEMBER_ROLE } from '../../constants'; +import { EVENT, RESOURCES, PROJECT_MEMBER_ROLE, COPILOT_REQUEST_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_APPLICATION_STATUS } from '../../constants'; import { PERMISSION, PROJECT_TO_TOPCODER_ROLES_MATRIX } from '../../permissions/constants'; +import { Op } from 'sequelize'; /** * API to update a project member. @@ -50,10 +51,10 @@ module.exports = [ let previousValue; // let newValue; - models.sequelize.transaction(() => models.ProjectMember.findOne({ + models.sequelize.transaction((_transaction) => models.ProjectMember.findOne({ where: { id: memberRecordId, projectId }, }) - .then((_member) => { + .then(async (_member) => { if (!_member) { // handle 404 const err = new Error(`project member not found for project id ${projectId} ` + @@ -80,6 +81,77 @@ module.exports = [ if (updatedProps.role === previousValue.role && (_.isUndefined(updatedProps.isPrimary) || updatedProps.isPrimary === previousValue.isPrimary)) { + + const allCopilotRequests = await models.CopilotRequest.findAll({ + where: { + projectId, + }, + transaction: _transaction, + }); + + req.log.debug(`all copilot requests ${JSON.stringify(allCopilotRequests)}`); + + await models.CopilotRequest.update({ + status: COPILOT_REQUEST_STATUS.FULFILLED, + }, { + where: { + id: { + [Op.in]: allCopilotRequests.map(item => item.id), + } + }, + transaction: _transaction, + }); + + req.log.debug(`updated all copilot requests`); + + const copilotOpportunites = await models.CopilotOpportunity.findAll({ + where: { + copilotRequestId: { + [Op.in]: allCopilotRequests.map(item => item.id), + }, + }, + transaction: _transaction, + }); + + req.log.debug(`all copilot opportunities ${JSON.stringify(copilotOpportunites)}`); + + await models.CopilotOpportunity.update({ + status: COPILOT_OPPORTUNITY_STATUS.COMPLETED, + }, { + where: { + id: { + [Op.in]: copilotOpportunites.map(item => item.id), + } + }, + transaction: _transaction, + }); + + req.log.debug(`updated all copilot opportunities`); + + const allCopilotApplications = await models.CopilotApplication.findAll({ + where: { + opportunityId: { + [Op.in]: copilotOpportunites.map(item => item.id), + }, + }, + transaction: _transaction, + }); + + req.log.debug(`all copilot applications ${JSON.stringify(allCopilotApplications)}`); + + await models.CopilotApplication.update({ + status: COPILOT_APPLICATION_STATUS.CANCELED, + }, { + where: { + id: { + [Op.in]: allCopilotApplications.map(item => item.id), + }, + }, + transaction: _transaction, + }); + + req.log.debug(`updated all copilot applications`); + return Promise.resolve(); } From 13c8c397a3b7197cfcb34dcd702ff53575a9230f Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 25 Jul 2025 23:29:10 +0200 Subject: [PATCH 052/105] fix: added error string and already assigned role --- src/routes/projectMembers/update.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index 527f6915..b8cb3f7f 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -77,6 +77,8 @@ module.exports = [ return Promise.reject(err); } + req.log.debug(`updated props ${JSON.stringify(updatedProps)}`); + req.log.debug(`previous values ${JSON.stringify(previousValue)}`); // no updates if no change if (updatedProps.role === previousValue.role && (_.isUndefined(updatedProps.isPrimary) || From f73f444870cd0676211ab709115ad3f1e20d3b3e Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Sun, 27 Jul 2025 23:43:29 +0200 Subject: [PATCH 053/105] feat: modifications on copilot addition to project --- src/routes/projectMemberInvites/create.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 896ed232..ac0461d5 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -339,14 +339,13 @@ module.exports = [ req.log.debug(`Existing Project Members: ${JSON.stringify(projectMembers)}`); const existingProjectMembersMap = projectMembers.reduce((acc, current) => { - return { - ...acc, + return Object.assign({}, acc, { [current.id]: current, - }; + }); }, {}); req.log.debug(`Existing Project Members Map: ${JSON.stringify(existingProjectMembersMap)}`); - + _.remove(inviteUserIds, u => _.some(members, (m) => { const isPresent = m.userId === u; if (isPresent) { From 7fce661ac6f2fb9161281f5eafd6922407e47ea7 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Jul 2025 00:11:10 +0200 Subject: [PATCH 054/105] feat: modifications on copilot addition to project --- src/routes/projectMemberInvites/create.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index ac0461d5..60e0a20f 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -324,8 +324,7 @@ module.exports = [ if (inviteUserIds) { const existingMembers = _.some(members, (m) => { - const isPresent = m.userId === u; - return isPresent; + return inviteUserIds.includes(m.userId); }); const projectMembers = await models.ProjectMember.findAll({ From 1f2cba440f5f4807437ed9d3419927cf4331fee6 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Jul 2025 00:33:59 +0200 Subject: [PATCH 055/105] feat: modifications on copilot addition to project --- src/routes/projectMemberInvites/create.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 60e0a20f..34e8b519 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -323,10 +323,12 @@ module.exports = [ const errorMessageForAlreadyMemberUser = 'User with such handle is already a member of the team.'; if (inviteUserIds) { - const existingMembers = _.some(members, (m) => { + const existingMembers = _.filter(members, (m) => { return inviteUserIds.includes(m.userId); }); + req.log.debug(`Existing members: ${JSON.stringify(existingMembers)}`); + const projectMembers = await models.ProjectMember.findAll({ where: { userId: { From 1266652a081ffe1f492bf2641001760158d8f5e5 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Jul 2025 00:55:54 +0200 Subject: [PATCH 056/105] feat: modifications on copilot addition to project --- src/routes/projectMemberInvites/create.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 34e8b519..2ec5a8fc 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -333,7 +333,8 @@ module.exports = [ where: { userId: { [Op.in]: existingMembers.map(item => item.userId), - } + }, + projectId, } }); @@ -341,7 +342,7 @@ module.exports = [ const existingProjectMembersMap = projectMembers.reduce((acc, current) => { return Object.assign({}, acc, { - [current.id]: current, + [current.userId]: current, }); }, {}); From c701b1e80101a76e8a1c2e501b0cec78eb3364ae Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Jul 2025 01:28:51 +0200 Subject: [PATCH 057/105] fix: complete the copilot requests if the incoming role is observer or customer --- src/routes/projectMembers/update.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index b8cb3f7f..e92b0bfd 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -28,6 +28,7 @@ const updateProjectMemberValdiations = { PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT, PROJECT_MEMBER_ROLE.PROJECT_MANAGER, ).required(), + action: Joi.string().optional(), }), query: { fields: Joi.string().optional(), @@ -80,7 +81,7 @@ module.exports = [ req.log.debug(`updated props ${JSON.stringify(updatedProps)}`); req.log.debug(`previous values ${JSON.stringify(previousValue)}`); // no updates if no change - if (updatedProps.role === previousValue.role && + if ((updatedProps.role === previousValue.role || updatedProps.action === 'overwrite') && (_.isUndefined(updatedProps.isPrimary) || updatedProps.isPrimary === previousValue.isPrimary)) { From 1d19d150dd622b66731c93b6d8a2032e074d6634 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Jul 2025 01:29:18 +0200 Subject: [PATCH 058/105] fix: complete the copilot requests if the incoming role is observer or customer --- src/routes/projectMembers/update.js | 150 +++++++++++++++------------- 1 file changed, 78 insertions(+), 72 deletions(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index e92b0bfd..25ac179c 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -35,6 +35,78 @@ const updateProjectMemberValdiations = { }, }; +const completeAllCopilotRequests = async (req, projectId, _transaction) => { + const allCopilotRequests = await models.CopilotRequest.findAll({ + where: { + projectId, + }, + transaction: _transaction, + }); + + req.log.debug(`all copilot requests ${JSON.stringify(allCopilotRequests)}`); + + await models.CopilotRequest.update({ + status: COPILOT_REQUEST_STATUS.FULFILLED, + }, { + where: { + id: { + [Op.in]: allCopilotRequests.map(item => item.id), + } + }, + transaction: _transaction, + }); + + req.log.debug(`updated all copilot requests`); + + const copilotOpportunites = await models.CopilotOpportunity.findAll({ + where: { + copilotRequestId: { + [Op.in]: allCopilotRequests.map(item => item.id), + }, + }, + transaction: _transaction, + }); + + req.log.debug(`all copilot opportunities ${JSON.stringify(copilotOpportunites)}`); + + await models.CopilotOpportunity.update({ + status: COPILOT_OPPORTUNITY_STATUS.COMPLETED, + }, { + where: { + id: { + [Op.in]: copilotOpportunites.map(item => item.id), + } + }, + transaction: _transaction, + }); + + req.log.debug(`updated all copilot opportunities`); + + const allCopilotApplications = await models.CopilotApplication.findAll({ + where: { + opportunityId: { + [Op.in]: copilotOpportunites.map(item => item.id), + }, + }, + transaction: _transaction, + }); + + req.log.debug(`all copilot applications ${JSON.stringify(allCopilotApplications)}`); + + await models.CopilotApplication.update({ + status: COPILOT_APPLICATION_STATUS.CANCELED, + }, { + where: { + id: { + [Op.in]: allCopilotApplications.map(item => item.id), + }, + }, + transaction: _transaction, + }); + + req.log.debug(`updated all copilot applications`); +}; + module.exports = [ // handles request validations validate(updateProjectMemberValdiations), @@ -84,77 +156,7 @@ module.exports = [ if ((updatedProps.role === previousValue.role || updatedProps.action === 'overwrite') && (_.isUndefined(updatedProps.isPrimary) || updatedProps.isPrimary === previousValue.isPrimary)) { - - const allCopilotRequests = await models.CopilotRequest.findAll({ - where: { - projectId, - }, - transaction: _transaction, - }); - - req.log.debug(`all copilot requests ${JSON.stringify(allCopilotRequests)}`); - - await models.CopilotRequest.update({ - status: COPILOT_REQUEST_STATUS.FULFILLED, - }, { - where: { - id: { - [Op.in]: allCopilotRequests.map(item => item.id), - } - }, - transaction: _transaction, - }); - - req.log.debug(`updated all copilot requests`); - - const copilotOpportunites = await models.CopilotOpportunity.findAll({ - where: { - copilotRequestId: { - [Op.in]: allCopilotRequests.map(item => item.id), - }, - }, - transaction: _transaction, - }); - - req.log.debug(`all copilot opportunities ${JSON.stringify(copilotOpportunites)}`); - - await models.CopilotOpportunity.update({ - status: COPILOT_OPPORTUNITY_STATUS.COMPLETED, - }, { - where: { - id: { - [Op.in]: copilotOpportunites.map(item => item.id), - } - }, - transaction: _transaction, - }); - - req.log.debug(`updated all copilot opportunities`); - - const allCopilotApplications = await models.CopilotApplication.findAll({ - where: { - opportunityId: { - [Op.in]: copilotOpportunites.map(item => item.id), - }, - }, - transaction: _transaction, - }); - - req.log.debug(`all copilot applications ${JSON.stringify(allCopilotApplications)}`); - - await models.CopilotApplication.update({ - status: COPILOT_APPLICATION_STATUS.CANCELED, - }, { - where: { - id: { - [Op.in]: allCopilotApplications.map(item => item.id), - }, - }, - transaction: _transaction, - }); - - req.log.debug(`updated all copilot applications`); - + await completeAllCopilotRequests(req, projectId, _transaction); return Promise.resolve(); } @@ -196,9 +198,13 @@ module.exports = [ }); }) .then(() => projectMember.reload(projectMember.id)) - .then(() => { + .then(async () => { projectMember = projectMember.get({ plain: true }); projectMember = _.omit(projectMember, ['deletedAt']); + + if (['observer', 'customer'].includes(updatedProps.role)) { + await completeAllCopilotRequests(req, projectId, _transaction); + } }) .then(() => ( util.getObjectsWithMemberDetails([projectMember], fields, req) From af842b09b81196575f0838fd0113dec44d463964 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Jul 2025 01:55:37 +0200 Subject: [PATCH 059/105] fix: complete the copilot requests if the incoming role is observer or customer --- src/routes/projectMembers/update.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index 25ac179c..f5208267 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -119,6 +119,7 @@ module.exports = [ let updatedProps = req.body; const projectId = _.parseInt(req.params.projectId); const memberRecordId = _.parseInt(req.params.id); + const action = updatedProps.action; updatedProps = _.pick(updatedProps, ['isPrimary', 'role']); const fields = req.query.fields ? req.query.fields.split(',') : null; @@ -153,7 +154,7 @@ module.exports = [ req.log.debug(`updated props ${JSON.stringify(updatedProps)}`); req.log.debug(`previous values ${JSON.stringify(previousValue)}`); // no updates if no change - if ((updatedProps.role === previousValue.role || updatedProps.action === 'overwrite') && + if ((updatedProps.role === previousValue.role || action === 'overwrite') && (_.isUndefined(updatedProps.isPrimary) || updatedProps.isPrimary === previousValue.isPrimary)) { await completeAllCopilotRequests(req, projectId, _transaction); From b2ac65f5a32f22cd19e3638842d2fd6d65264632 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Mon, 28 Jul 2025 09:17:20 +0300 Subject: [PATCH 060/105] correct copilots slack --- config/production.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/production.json b/config/production.json index 67418d53..9715f1cd 100644 --- a/config/production.json +++ b/config/production.json @@ -5,5 +5,5 @@ "sfdcBillingAccountNameField": "Billing_Account_name__c", "sfdcBillingAccountMarkupField": "Mark_up__c", "sfdcBillingAccountActiveField": "Active__c", - "copilotsSlackEmail": "mem-copilots-aaaaaa4m4blyjxrlrv7yr37y6u@topcoder.slack.com" + "copilotsSlackEmail": "mem-ops-copilot-aaaadbvbjvvek6ojnwpfdh5qq4@topcoder.slack.com" } From 0d02782d4d04fc7b8a5d3968bff06c99411b5908 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Jul 2025 13:07:57 +0200 Subject: [PATCH 061/105] fix: complete the copilot requests if the incoming role is observer or customer --- src/routes/projectMembers/update.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index f5208267..f12f8913 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -154,7 +154,7 @@ module.exports = [ req.log.debug(`updated props ${JSON.stringify(updatedProps)}`); req.log.debug(`previous values ${JSON.stringify(previousValue)}`); // no updates if no change - if ((updatedProps.role === previousValue.role || action === 'overwrite') && + if ((updatedProps.role === previousValue.role || action === 'complete-copilot-requests') && (_.isUndefined(updatedProps.isPrimary) || updatedProps.isPrimary === previousValue.isPrimary)) { await completeAllCopilotRequests(req, projectId, _transaction); From ce575c75479e1be16452c3bc798458d74e579980 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Jul 2025 13:12:00 +0200 Subject: [PATCH 062/105] fix: action string --- src/routes/projectMemberInvites/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 2ec5a8fc..973fb16d 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -5,6 +5,7 @@ import _ from 'lodash'; import Joi from 'joi'; import config from 'config'; import { middleware as tcMiddleware } from 'tc-core-library-js'; +import { Op } from 'sequelize'; import models from '../../models'; import util from '../../util'; import { @@ -18,7 +19,6 @@ import { } from '../../constants'; import { createEvent } from '../../services/busApi'; import { PERMISSION, PROJECT_TO_TOPCODER_ROLES_MATRIX } from '../../permissions/constants'; -import { Op } from 'sequelize'; const ALLOWED_FIELDS = _.keys(models.ProjectMemberInvite.rawAttributes).concat(['handle']); From 2f2e162c6452a386db478506a42b5127ecfea21f Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Jul 2025 23:17:19 +0200 Subject: [PATCH 063/105] fix: send email to project manager on copilot invite acceptation --- .circleci/config.yml | 2 +- src/constants.js | 1 + src/routes/projectMemberInvites/update.js | 68 ++++++++++++++++++++++- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e5ab511c..268aff93 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'pm-1506'] + only: ['develop', 'migration-setup', 'pm-1510'] - deployProd: context : org-global filters: diff --git a/src/constants.js b/src/constants.js index bfc93281..a596983c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -311,6 +311,7 @@ export const TEMPLATE_IDS = { APPLY_COPILOT: 'd-d7c1f48628654798a05c8e09e52db14f', CREATE_REQUEST: 'd-3efdc91da580479d810c7acd50a4c17f', PROJECT_MEMBER_INVITED: 'd-b47a25b103604bc28fc0ce77e77fb681', + INFORM_PM_COPILOT_APPLICATION_ACCEPTED: 'd-b35d073e302b4279a1bd208fcfe96f58', } export const REGEX = { URL: /^(http(s?):\/\/)?(www\.)?[a-zA-Z0-9\.\-\_]+(\.[a-zA-Z]{2,15})+(\:[0-9]{2,5})?(\/[a-zA-Z0-9\_\-\s\.\/\?\%\#\&\=;]*)?$/, // eslint-disable-line diff --git a/src/routes/projectMemberInvites/update.js b/src/routes/projectMemberInvites/update.js index 9f21b1c8..b7b67eaa 100644 --- a/src/routes/projectMemberInvites/update.js +++ b/src/routes/projectMemberInvites/update.js @@ -2,10 +2,12 @@ import validate from 'express-validation'; import _ from 'lodash'; import Joi from 'joi'; import { Op } from 'sequelize'; +import config from 'config'; + import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; -import { INVITE_STATUS, EVENT, RESOURCES, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, INVITE_SOURCE } from '../../constants'; +import { INVITE_STATUS, EVENT, RESOURCES, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, INVITE_SOURCE, CONNECT_NOTIFICATION_EVENT, TEMPLATE_IDS } from '../../constants'; import { PERMISSION } from '../../permissions/constants'; @@ -264,6 +266,70 @@ module.exports = [ } await t.commit(); + if (source === 'copilot_portal' && invite.applicationId) { + const pmRole = await util.getRolesByRoleName(USER_ROLE.PROJECT_MANAGER, req.log, req.id); + const { subjects = [] } = await util.getRoleInfo(pmRole[0], req.log, req.id); + + const userDetails = await util.getMemberDetailsByUserIds([opportunity.createdBy, invite.userId], req.log, req.id); + const creator = userDetails[0]; + const invitee = userDetails[1]; + const listOfSubjects = subjects; + if (creator && creator.email) { + const isCreatorPartofSubjects = subjects.find(item => { + if (!item.email) { + return false; + } + + return item.email.toLowerCase() === creator[0].email.toLowerCase(); + }); + if (!isCreatorPartofSubjects) { + listOfSubjects.push({ + email: creator[0].email, + handle: creator[0].handle, + }); + } + } + + + const application = await models.CopilotApplication.findOne({ + where: { + id: invite.applicationId, + }, + }); + + const opportunity = await models.CopilotOpportunity.findOne({ + where: { + id: application.opportunityId, + }, + include: [ + { + model: models.CopilotRequest, + as: 'copilotRequest', + }, + ], + }); + + const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL; + const copilotPortalUrl = config.get('copilotPortalUrl'); + const requestData = opportunity.copilotRequest.data; + listOfSubjects.forEach((subject) => { + createEvent(emailEventType, { + data: { + user_name: subject.handle, + opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}#applications`, + work_manager_url: config.get('workManagerUrl'), + opportunity_type: getCopilotTypeLabel(requestData.projectType), + opportunity_title: requestData.opportunityTitle, + copilot_handle: invitee ? invitee.handle : "", + }, + sendgrid_template_id: TEMPLATE_IDS.INFORM_PM_COPILOT_APPLICATION_ACCEPTED, + recipients: [subject.email], + version: 'v3', + }, req.log); + }); + + + } return res.json(util.postProcessInvites('$.email', updatedInvite, req)); } catch (e) { await t.rollback(); From 287e14fa6cca2d590b989925b6dffbd83407ed51 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Jul 2025 23:49:01 +0200 Subject: [PATCH 064/105] fix: send email to project manager on copilot invite acceptation --- src/routes/projectMemberInvites/update.js | 7 ++++--- src/routes/projectMembers/update.js | 9 +++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/routes/projectMemberInvites/update.js b/src/routes/projectMemberInvites/update.js index b7b67eaa..ab8e2a02 100644 --- a/src/routes/projectMemberInvites/update.js +++ b/src/routes/projectMemberInvites/update.js @@ -9,6 +9,8 @@ import models from '../../models'; import util from '../../util'; import { INVITE_STATUS, EVENT, RESOURCES, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, INVITE_SOURCE, CONNECT_NOTIFICATION_EVENT, TEMPLATE_IDS } from '../../constants'; import { PERMISSION } from '../../permissions/constants'; +import { getCopilotTypeLabel } from '../../utils/copilot'; +import { createEvent } from '../../services/busApi'; /** @@ -265,7 +267,6 @@ module.exports = [ }) } - await t.commit(); if (source === 'copilot_portal' && invite.applicationId) { const pmRole = await util.getRolesByRoleName(USER_ROLE.PROJECT_MANAGER, req.log, req.id); const { subjects = [] } = await util.getRoleInfo(pmRole[0], req.log, req.id); @@ -327,9 +328,9 @@ module.exports = [ version: 'v3', }, req.log); }); - - } + + await t.commit(); return res.json(util.postProcessInvites('$.email', updatedInvite, req)); } catch (e) { await t.rollback(); diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index f12f8913..fa1ed5a6 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -105,6 +105,8 @@ const completeAllCopilotRequests = async (req, projectId, _transaction) => { }); req.log.debug(`updated all copilot applications`); + + await _transaction.commit(); }; module.exports = [ @@ -125,7 +127,7 @@ module.exports = [ let previousValue; // let newValue; - models.sequelize.transaction((_transaction) => models.ProjectMember.findOne({ + models.sequelize.transaction(async (_transaction) => models.ProjectMember.findOne({ where: { id: memberRecordId, projectId }, }) .then(async (_member) => { @@ -227,6 +229,9 @@ module.exports = [ req.log.debug('updated project member', projectMember); res.json(memberWithDetails || projectMember); }) - .catch(err => next(err))); + .catch(async (err) => { + await _transaction.rollback(); + return next(err); + })); }, ]; From 21977ef22c587a028973726536e137bb701ba4e8 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Jul 2025 00:09:03 +0200 Subject: [PATCH 065/105] fix: send email to project manager on copilot invite acceptation --- src/routes/projectMemberInvites/update.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/projectMemberInvites/update.js b/src/routes/projectMemberInvites/update.js index ab8e2a02..001f14b3 100644 --- a/src/routes/projectMemberInvites/update.js +++ b/src/routes/projectMemberInvites/update.js @@ -7,7 +7,7 @@ import config from 'config'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; -import { INVITE_STATUS, EVENT, RESOURCES, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, INVITE_SOURCE, CONNECT_NOTIFICATION_EVENT, TEMPLATE_IDS } from '../../constants'; +import { INVITE_STATUS, EVENT, RESOURCES, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, INVITE_SOURCE, CONNECT_NOTIFICATION_EVENT, TEMPLATE_IDS, USER_ROLE } from '../../constants'; import { PERMISSION } from '../../permissions/constants'; import { getCopilotTypeLabel } from '../../utils/copilot'; import { createEvent } from '../../services/busApi'; @@ -281,12 +281,12 @@ module.exports = [ return false; } - return item.email.toLowerCase() === creator[0].email.toLowerCase(); + return item.email.toLowerCase() === creator.email.toLowerCase(); }); if (!isCreatorPartofSubjects) { listOfSubjects.push({ - email: creator[0].email, - handle: creator[0].handle, + email: creator.email, + handle: creator.handle, }); } } From b4dbde56a857d9b52fadbfe925ea217aaf4600a5 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Jul 2025 00:35:18 +0200 Subject: [PATCH 066/105] fix: send email to project manager on copilot invite acceptation --- src/routes/projectMemberInvites/update.js | 36 +++++++++++------------ 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/routes/projectMemberInvites/update.js b/src/routes/projectMemberInvites/update.js index 001f14b3..9f54e729 100644 --- a/src/routes/projectMemberInvites/update.js +++ b/src/routes/projectMemberInvites/update.js @@ -268,6 +268,23 @@ module.exports = [ } if (source === 'copilot_portal' && invite.applicationId) { + const application = await models.CopilotApplication.findOne({ + where: { + id: invite.applicationId, + }, + }); + + const opportunity = await models.CopilotOpportunity.findOne({ + where: { + id: application.opportunityId, + }, + include: [ + { + model: models.CopilotRequest, + as: 'copilotRequest', + }, + ], + }); const pmRole = await util.getRolesByRoleName(USER_ROLE.PROJECT_MANAGER, req.log, req.id); const { subjects = [] } = await util.getRoleInfo(pmRole[0], req.log, req.id); @@ -291,25 +308,6 @@ module.exports = [ } } - - const application = await models.CopilotApplication.findOne({ - where: { - id: invite.applicationId, - }, - }); - - const opportunity = await models.CopilotOpportunity.findOne({ - where: { - id: application.opportunityId, - }, - include: [ - { - model: models.CopilotRequest, - as: 'copilotRequest', - }, - ], - }); - const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL; const copilotPortalUrl = config.get('copilotPortalUrl'); const requestData = opportunity.copilotRequest.data; From fc0bffea83c0166987ae9548cb5df7b5dbe667f3 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Jul 2025 01:05:40 +0200 Subject: [PATCH 067/105] fix: send email to project manager on copilot invite acceptation --- src/routes/projectMemberInvites/update.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/routes/projectMemberInvites/update.js b/src/routes/projectMemberInvites/update.js index 9f54e729..d7ff1d66 100644 --- a/src/routes/projectMemberInvites/update.js +++ b/src/routes/projectMemberInvites/update.js @@ -288,9 +288,10 @@ module.exports = [ const pmRole = await util.getRolesByRoleName(USER_ROLE.PROJECT_MANAGER, req.log, req.id); const { subjects = [] } = await util.getRoleInfo(pmRole[0], req.log, req.id); - const userDetails = await util.getMemberDetailsByUserIds([opportunity.createdBy, invite.userId], req.log, req.id); - const creator = userDetails[0]; - const invitee = userDetails[1]; + const creatorDetails = await util.getMemberDetailsByUserIds([opportunity.createdBy], req.log, req.id); + const inviteeDetails = await util.getMemberDetailsByUserIds([application.userId], req.log, req.id); + const creator = creatorDetails[0]; + const invitee = inviteeDetails[0]; const listOfSubjects = subjects; if (creator && creator.email) { const isCreatorPartofSubjects = subjects.find(item => { From ccceb703dba9f449fa88450da275a09905af725b Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Jul 2025 18:52:19 +0200 Subject: [PATCH 068/105] send email to copilot who got selected --- src/constants.js | 1 + src/routes/projectMembers/update.js | 41 ++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/constants.js b/src/constants.js index a596983c..1b727459 100644 --- a/src/constants.js +++ b/src/constants.js @@ -312,6 +312,7 @@ export const TEMPLATE_IDS = { CREATE_REQUEST: 'd-3efdc91da580479d810c7acd50a4c17f', PROJECT_MEMBER_INVITED: 'd-b47a25b103604bc28fc0ce77e77fb681', INFORM_PM_COPILOT_APPLICATION_ACCEPTED: 'd-b35d073e302b4279a1bd208fcfe96f58', + COPILOT_ALREADY_PART_OF_PROJECT: 'd-003d41cdc9de4bbc9e14538e8f2e0585', } export const REGEX = { URL: /^(http(s?):\/\/)?(www\.)?[a-zA-Z0-9\.\-\_]+(\.[a-zA-Z]{2,15})+(\:[0-9]{2,5})?(\/[a-zA-Z0-9\_\-\s\.\/\?\%\#\&\=;]*)?$/, // eslint-disable-line diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index fa1ed5a6..924d49c9 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -2,12 +2,15 @@ import validate from 'express-validation'; import _ from 'lodash'; import Joi from 'joi'; +import config from 'config'; +import moment from 'moment'; +import { Op } from 'sequelize'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; -import { EVENT, RESOURCES, PROJECT_MEMBER_ROLE, COPILOT_REQUEST_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_APPLICATION_STATUS } from '../../constants'; +import { EVENT, RESOURCES, PROJECT_MEMBER_ROLE, COPILOT_REQUEST_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_APPLICATION_STATUS, USER_ROLE, CONNECT_NOTIFICATION_EVENT, TEMPLATE_IDS } from '../../constants'; import { PERMISSION, PROJECT_TO_TOPCODER_ROLES_MATRIX } from '../../permissions/constants'; -import { Op } from 'sequelize'; + /** * API to update a project member. @@ -35,7 +38,7 @@ const updateProjectMemberValdiations = { }, }; -const completeAllCopilotRequests = async (req, projectId, _transaction) => { +const completeAllCopilotRequests = async (req, projectId, _transaction, _member) => { const allCopilotRequests = await models.CopilotRequest.findAll({ where: { projectId, @@ -107,6 +110,36 @@ const completeAllCopilotRequests = async (req, projectId, _transaction) => { req.log.debug(`updated all copilot applications`); await _transaction.commit(); + + const memberDetails = await util.getMemberDetailsByUserIds([_member.userId], req.log, req.id); + + req.log.debug(`member details: ${JSON.stringify(memberDetails)}`); + + const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL; + const copilotPortalUrl = config.get('copilotPortalUrl'); + allCopilotRequests.forEach((request) => { + const requestData = request.data; + + req.log.debug(`Copilot request data: ${requestData}`); + const opportunity = copilotOpportunites.find(item => item.copilotRequestId === request.id); + + req.log.debug(`Opportunity: ${opportunity}`); + createEvent(emailEventType, { + data: { + opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`, + work_manager_url: config.get('workManagerUrl'), + opportunity_type: getCopilotTypeLabel(requestData.projectType), + opportunity_title: requestData.opportunityTitle, + start_date: moment.utc(requestData.startDate).format('DD-MM-YYYY'), + user_name: memberDetails ? memberDetails.handle : "", + }, + sendgrid_template_id: TEMPLATE_IDS.COPILOT_ALREADY_PART_OF_PROJECT, + recipients: [memberDetails.email], + version: 'v3', + }, req.log); + + req.log.debug(`Sent email to ${memberDetails.email}`); + }); }; module.exports = [ @@ -206,7 +239,7 @@ module.exports = [ projectMember = _.omit(projectMember, ['deletedAt']); if (['observer', 'customer'].includes(updatedProps.role)) { - await completeAllCopilotRequests(req, projectId, _transaction); + await completeAllCopilotRequests(req, projectId, _transaction, _member); } }) .then(() => ( From d56f2335df743f8d73dd75485f85860f1d298a59 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Jul 2025 19:00:55 +0200 Subject: [PATCH 069/105] send email to copilot who got selected --- src/routes/projectMembers/update.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index 924d49c9..e6535683 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -94,6 +94,9 @@ const completeAllCopilotRequests = async (req, projectId, _transaction, _member) transaction: _transaction, }); + const memberApplication = allCopilotApplications.find(app => app.userId === _member.userId); + const applicationsWithoutMemberApplication = allCopilotApplications.filter(app => app.userId !== _member.userId); + req.log.debug(`all copilot applications ${JSON.stringify(allCopilotApplications)}`); await models.CopilotApplication.update({ @@ -101,12 +104,24 @@ const completeAllCopilotRequests = async (req, projectId, _transaction, _member) }, { where: { id: { - [Op.in]: allCopilotApplications.map(item => item.id), + [Op.in]: applicationsWithoutMemberApplication.map(item => item.id), }, }, transaction: _transaction, }); + // If the invited member + if (memberApplication) { + await models.CopilotApplication.update({ + status: COPILOT_APPLICATION_STATUS.ACCEPTED, + }, { + where: { + id: memberApplication.id, + }, + transaction: _transaction, + }); + } + req.log.debug(`updated all copilot applications`); await _transaction.commit(); From 37f740c78053169753236a6b6165cf073af9f8ae Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Jul 2025 20:11:23 +0200 Subject: [PATCH 070/105] fix: allow action to be empty string --- src/routes/projectMembers/update.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index e6535683..ee093900 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -31,7 +31,7 @@ const updateProjectMemberValdiations = { PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT, PROJECT_MEMBER_ROLE.PROJECT_MANAGER, ).required(), - action: Joi.string().optional(), + action: Joi.string().allow('').optional(), }), query: { fields: Joi.string().optional(), From 993ea573a9c56c581072ceac785aac525c352bf6 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Jul 2025 20:34:08 +0200 Subject: [PATCH 071/105] fix: allow action to be empty string --- src/routes/projectMembers/update.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index ee093900..ec4d55a6 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -207,7 +207,7 @@ module.exports = [ if ((updatedProps.role === previousValue.role || action === 'complete-copilot-requests') && (_.isUndefined(updatedProps.isPrimary) || updatedProps.isPrimary === previousValue.isPrimary)) { - await completeAllCopilotRequests(req, projectId, _transaction); + await completeAllCopilotRequests(req, projectId, _transaction, _member); return Promise.resolve(); } From b3e4f8eb3d18d1fc7e795c16ef81a94ba4af051d Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Jul 2025 20:49:56 +0200 Subject: [PATCH 072/105] fix: allow action to be empty string --- src/routes/projectMembers/update.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index ec4d55a6..8e34ffc3 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -10,6 +10,7 @@ import models from '../../models'; import util from '../../util'; import { EVENT, RESOURCES, PROJECT_MEMBER_ROLE, COPILOT_REQUEST_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_APPLICATION_STATUS, USER_ROLE, CONNECT_NOTIFICATION_EVENT, TEMPLATE_IDS } from '../../constants'; import { PERMISSION, PROJECT_TO_TOPCODER_ROLES_MATRIX } from '../../permissions/constants'; +import { createEvent } from '../../services/busApi'; /** @@ -135,10 +136,10 @@ const completeAllCopilotRequests = async (req, projectId, _transaction, _member) allCopilotRequests.forEach((request) => { const requestData = request.data; - req.log.debug(`Copilot request data: ${requestData}`); + req.log.debug(`Copilot request data: ${JSON.stringify(requestData)}`); const opportunity = copilotOpportunites.find(item => item.copilotRequestId === request.id); - req.log.debug(`Opportunity: ${opportunity}`); + req.log.debug(`Opportunity: ${JSON.stringify(opportunity)}`); createEvent(emailEventType, { data: { opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`, From 45629b5fe72e2179d2754aa7d3170b25504b3ea9 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Jul 2025 21:30:11 +0200 Subject: [PATCH 073/105] fix: allow action to be empty string --- src/routes/projectMembers/update.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index 8e34ffc3..26442031 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -125,8 +125,6 @@ const completeAllCopilotRequests = async (req, projectId, _transaction, _member) req.log.debug(`updated all copilot applications`); - await _transaction.commit(); - const memberDetails = await util.getMemberDetailsByUserIds([_member.userId], req.log, req.id); req.log.debug(`member details: ${JSON.stringify(memberDetails)}`); @@ -156,6 +154,8 @@ const completeAllCopilotRequests = async (req, projectId, _transaction, _member) req.log.debug(`Sent email to ${memberDetails.email}`); }); + + await _transaction.commit(); }; module.exports = [ From 5025fa16d6837ac82557f7bf4b2cae07a0ff6608 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Jul 2025 21:50:48 +0200 Subject: [PATCH 074/105] fix: allow action to be empty string --- src/routes/projectMembers/update.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index 26442031..c743d549 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -11,6 +11,7 @@ import util from '../../util'; import { EVENT, RESOURCES, PROJECT_MEMBER_ROLE, COPILOT_REQUEST_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_APPLICATION_STATUS, USER_ROLE, CONNECT_NOTIFICATION_EVENT, TEMPLATE_IDS } from '../../constants'; import { PERMISSION, PROJECT_TO_TOPCODER_ROLES_MATRIX } from '../../permissions/constants'; import { createEvent } from '../../services/busApi'; +import { getCopilotTypeLabel } from '../../utils/copilot'; /** From 7075d3fd5d90204da27af8eae10478a97630df32 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Jul 2025 22:22:13 +0200 Subject: [PATCH 075/105] fix: allow action to be empty string --- src/routes/projectMembers/update.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index c743d549..e38b6e14 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -44,6 +44,13 @@ const completeAllCopilotRequests = async (req, projectId, _transaction, _member) const allCopilotRequests = await models.CopilotRequest.findAll({ where: { projectId, + status: { + [Op.in]: [ + COPILOT_REQUEST_STATUS.APPROVED, + COPILOT_REQUEST_STATUS.NEW, + COPILOT_REQUEST_STATUS.SEEKING, + ], + } }, transaction: _transaction, }); @@ -127,8 +134,9 @@ const completeAllCopilotRequests = async (req, projectId, _transaction, _member) req.log.debug(`updated all copilot applications`); const memberDetails = await util.getMemberDetailsByUserIds([_member.userId], req.log, req.id); + const member = memberDetails[0]; - req.log.debug(`member details: ${JSON.stringify(memberDetails)}`); + req.log.debug(`member details: ${JSON.stringify(member)}`); const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL; const copilotPortalUrl = config.get('copilotPortalUrl'); @@ -146,14 +154,14 @@ const completeAllCopilotRequests = async (req, projectId, _transaction, _member) opportunity_type: getCopilotTypeLabel(requestData.projectType), opportunity_title: requestData.opportunityTitle, start_date: moment.utc(requestData.startDate).format('DD-MM-YYYY'), - user_name: memberDetails ? memberDetails.handle : "", + user_name: member ? member.handle : "", }, sendgrid_template_id: TEMPLATE_IDS.COPILOT_ALREADY_PART_OF_PROJECT, - recipients: [memberDetails.email], + recipients: [member.email], version: 'v3', }, req.log); - req.log.debug(`Sent email to ${memberDetails.email}`); + req.log.debug(`Sent email to ${member.email}`); }); await _transaction.commit(); From 683b3d6db5b64094df57544cc817b6a9ac2dd736 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 10:23:19 +0200 Subject: [PATCH 076/105] added existing membership to the copilot application --- src/routes/copilotOpportunityApply/list.js | 24 ++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index 69aea8fe..0ae756e7 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -9,11 +9,23 @@ const permissions = tcMiddleware.permissions; module.exports = [ permissions('copilotApplications.view'), - (req, res, next) => { + async (req, res, next) => { const canAccessAllApplications = util.hasRoles(req, ADMIN_ROLES) || util.hasProjectManagerRole(req); const userId = req.authUser.userId; const opportunityId = _.parseInt(req.params.id); + const opportunity = await models.CopilotOpportunity.findOne({ + where: { + id: opportunityId, + } + }); + + if (!opportunity) { + const err = new Error('No opportunity found'); + err.status = 404; + throw err; + } + let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'createdAt desc'; if (sort.indexOf(' ') === -1) { sort += ' asc'; @@ -41,7 +53,15 @@ module.exports = [ ], order: [[sortParams[0], sortParams[1]]], }) - .then(copilotApplications => res.json(copilotApplications)) + .then(copilotApplications => { + return models.ProjectMember.getActiveProjectMembers(opportunity.projectId).then((members) => { + return res.json(copilotApplications.map(application => { + return Object.assign({}, application, { + existingMembership: members.find(m => m.userId === application.userId), + }); + })); + }); + }) .catch((err) => { util.handleError('Error fetching copilot applications', err, req, next); }); From f0a889faa3a821ef9fc6514a86c381357292903a Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 10:47:43 +0200 Subject: [PATCH 077/105] added existing membership to the copilot application --- src/routes/copilotOpportunity/assign.js | 75 +++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index 5a3d0b3f..7b591fa8 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -45,11 +45,17 @@ module.exports = [ throw err; } + const copilotRequest = await models.CopilotRequest.findOne({ + where: { id: opportunity.copilotRequestId }, + transaction: t, + }); + const application = await models.CopilotApplication.findOne({ where: { id: applicationId, opportunityId: copilotOpportunityId }, transaction: t, }); + if (!application) { const err = new Error('No such application available'); err.status = 400; @@ -65,12 +71,69 @@ module.exports = [ const projectId = opportunity.projectId; const userId = application.userId; const activeMembers = await models.ProjectMember.getActiveProjectMembers(projectId, t); - - const existingUser = activeMembers.find(item => item.userId === userId); - if (existingUser && existingUser.role === 'copilot') { - const err = new Error(`User is already a copilot of this project`); - err.status = 400; - throw err; + const updateCopilotOpportunity = async () => { + await opportunity.update({ + status: COPILOT_OPPORTUNITY_STATUS.COMPLETED, + }, { + transaction: t, + }); + await application.update({ + status: COPILOT_APPLICATION_STATUS.ACCEPTED, + }, { + transaction: t, + }); + + await copilotRequest.update({ + status: COPILOT_REQUEST_STATUS.FULFILLED, + }, { + transaction: t, + }); + + await models.CopilotApplication.update({ + status: COPILOT_APPLICATION_STATUS.CANCELED, + }, { + where: { + projectId, + opportunityId: opportunity.id, + id: { + $ne: application.id, + }, + } + }); + }; + + const existingMember = activeMembers.find(item => item.userId === userId); + if (existingMember) { + if (['copilot', 'manager'].includes(existingMember.role)) { + + updateCopilotOpportunity(); + + } else { + await models.ProjectMember.update({ + role: 'copilot', + }, { + where: { + id: existingMember.id, + }, + }); + + const projectMember = await models.ProjectMember.findOne({ + where: { + id: existingMember.id, + }, + }); + + util.sendResourceToKafkaBus( + req, + EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED, + RESOURCES.PROJECT_MEMBER, + projectMember.get({ plain: true }), + existingMember); + + updateCopilotOpportunity(); + } + res.status(200).send({ id: applicationId }); + return; } const existingInvite = await models.ProjectMemberInvite.findAll({ From 4823330fd60891ff963d8b71f264a6818ad15643 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 10:50:49 +0200 Subject: [PATCH 078/105] added existing membership to the copilot application --- src/routes/copilotOpportunity/assign.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index 7b591fa8..f783a534 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -104,11 +104,12 @@ module.exports = [ const existingMember = activeMembers.find(item => item.userId === userId); if (existingMember) { + req.log.debug(`User already part of project: ${JSON.stringify(existingMember)}`); if (['copilot', 'manager'].includes(existingMember.role)) { - + req.log.debug(`User is a copilot or manager`); updateCopilotOpportunity(); - } else { + req.log.debug(`User has read/write role`); await models.ProjectMember.update({ role: 'copilot', }, { @@ -123,14 +124,16 @@ module.exports = [ }, }); + req.log.debug(`Updated project member: ${JSON.stringify(projectMember.get({plain: true}))}`); + util.sendResourceToKafkaBus( req, EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED, RESOURCES.PROJECT_MEMBER, projectMember.get({ plain: true }), existingMember); - - updateCopilotOpportunity(); + req.log.debug(`Member updated in kafka`); + updateCopilotOpportunity(); } res.status(200).send({ id: applicationId }); return; From 26dc288ad598e394ae5fe991b6ecfc6d50a3ff90 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 11:07:16 +0200 Subject: [PATCH 079/105] added existing membership to the copilot application --- src/routes/copilotOpportunityApply/list.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index 0ae756e7..9a7f2dae 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -55,7 +55,11 @@ module.exports = [ }) .then(copilotApplications => { return models.ProjectMember.getActiveProjectMembers(opportunity.projectId).then((members) => { - return res.json(copilotApplications.map(application => { + const applications = copilotApplications.get({plain: true}); + req.log.debug(`Fetched existing active members ${JSON.stringify(members)}`); + req.log.debug(`Applications ${JSON.stringify(applications)}`); + return res.json(applications.map(application => { + req.log.debug(`Existing member to application ${JSON.stringify(members.find(m => m.userId === application.userId))}`); return Object.assign({}, application, { existingMembership: members.find(m => m.userId === application.userId), }); From 59c6077b9af818a260dd02db97790208e696cb2e Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 13:05:43 +0200 Subject: [PATCH 080/105] added existing membership to the copilot application --- src/routes/copilotOpportunityApply/list.js | 75 +++++++++++----------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index 9a7f2dae..12c62b65 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -9,23 +9,11 @@ const permissions = tcMiddleware.permissions; module.exports = [ permissions('copilotApplications.view'), - async (req, res, next) => { + (req, res, next) => { const canAccessAllApplications = util.hasRoles(req, ADMIN_ROLES) || util.hasProjectManagerRole(req); const userId = req.authUser.userId; const opportunityId = _.parseInt(req.params.id); - const opportunity = await models.CopilotOpportunity.findOne({ - where: { - id: opportunityId, - } - }); - - if (!opportunity) { - const err = new Error('No opportunity found'); - err.status = 404; - throw err; - } - let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'createdAt desc'; if (sort.indexOf(' ') === -1) { sort += ' asc'; @@ -43,31 +31,42 @@ module.exports = [ canAccessAllApplications ? {} : { createdBy: userId }, ); - return models.CopilotApplication.findAll({ - where: whereCondition, - include: [ - { - model: models.CopilotOpportunity, - as: 'copilotOpportunity', - }, - ], - order: [[sortParams[0], sortParams[1]]], - }) - .then(copilotApplications => { - return models.ProjectMember.getActiveProjectMembers(opportunity.projectId).then((members) => { - const applications = copilotApplications.get({plain: true}); - req.log.debug(`Fetched existing active members ${JSON.stringify(members)}`); - req.log.debug(`Applications ${JSON.stringify(applications)}`); - return res.json(applications.map(application => { - req.log.debug(`Existing member to application ${JSON.stringify(members.find(m => m.userId === application.userId))}`); - return Object.assign({}, application, { - existingMembership: members.find(m => m.userId === application.userId), - }); - })); - }); + return models.CopilotOpportunity.findOne({ + where: { + id: opportunityId, + } + }).then((opportunity) => { + if (!opportunity) { + const err = new Error('No opportunity found'); + err.status = 404; + throw err; + } + return models.CopilotApplication.findAll({ + where: whereCondition, + include: [ + { + model: models.CopilotOpportunity, + as: 'copilotOpportunity', + }, + ], + order: [[sortParams[0], sortParams[1]]], }) - .catch((err) => { - util.handleError('Error fetching copilot applications', err, req, next); - }); + .then(copilotApplications => { + return models.ProjectMember.getActiveProjectMembers(opportunity.projectId).then((members) => { + const applications = copilotApplications.get({plain: true}); + req.log.debug(`Fetched existing active members ${JSON.stringify(members)}`); + req.log.debug(`Applications ${JSON.stringify(applications)}`); + return res.json(applications.map(application => { + req.log.debug(`Existing member to application ${JSON.stringify(members.find(m => m.userId === application.userId))}`); + return Object.assign({}, application, { + existingMembership: members.find(m => m.userId === application.userId), + }); + })); + }); + }) + }) + .catch((err) => { + util.handleError('Error fetching copilot applications', err, req, next); + }); }, ]; From 2ba33ac82dea701ef068d258c925b60e1b3a440e Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 13:20:11 +0200 Subject: [PATCH 081/105] added existing membership to the copilot application --- src/routes/copilotOpportunityApply/list.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index 12c62b65..c3856da4 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -52,11 +52,11 @@ module.exports = [ order: [[sortParams[0], sortParams[1]]], }) .then(copilotApplications => { + req.log.debug(`CopilotApplications ${JSON.stringify(copilotApplications)}`); return models.ProjectMember.getActiveProjectMembers(opportunity.projectId).then((members) => { - const applications = copilotApplications.get({plain: true}); req.log.debug(`Fetched existing active members ${JSON.stringify(members)}`); - req.log.debug(`Applications ${JSON.stringify(applications)}`); - return res.json(applications.map(application => { + req.log.debug(`Applications ${JSON.stringify(copilotApplications)}`); + return res.json(copilotApplications.map(application => { req.log.debug(`Existing member to application ${JSON.stringify(members.find(m => m.userId === application.userId))}`); return Object.assign({}, application, { existingMembership: members.find(m => m.userId === application.userId), From 04b4e37ceaf50767669e0322ebf116edc8c351f1 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 13:46:29 +0200 Subject: [PATCH 082/105] added existing membership to the copilot application --- src/routes/copilotOpportunityApply/list.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index c3856da4..5312bebb 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -56,12 +56,15 @@ module.exports = [ return models.ProjectMember.getActiveProjectMembers(opportunity.projectId).then((members) => { req.log.debug(`Fetched existing active members ${JSON.stringify(members)}`); req.log.debug(`Applications ${JSON.stringify(copilotApplications)}`); - return res.json(copilotApplications.map(application => { + const enrichedApplications = copilotApplications.map(application => { req.log.debug(`Existing member to application ${JSON.stringify(members.find(m => m.userId === application.userId))}`); return Object.assign({}, application, { existingMembership: members.find(m => m.userId === application.userId), }); - })); + }); + + req.log.debug(`Enriched Applications ${JSON.stringify(enrichedApplications)}`); + return res.json(enrichedApplications); }); }) }) From f2a8c89c4af4e5c7362782e0c464666d463667a8 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 14:13:22 +0200 Subject: [PATCH 083/105] added existing membership to the copilot application --- src/routes/copilotOpportunityApply/list.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index 5312bebb..bfaa6109 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -57,10 +57,10 @@ module.exports = [ req.log.debug(`Fetched existing active members ${JSON.stringify(members)}`); req.log.debug(`Applications ${JSON.stringify(copilotApplications)}`); const enrichedApplications = copilotApplications.map(application => { - req.log.debug(`Existing member to application ${JSON.stringify(members.find(m => m.userId === application.userId))}`); - return Object.assign({}, application, { - existingMembership: members.find(m => m.userId === application.userId), - }); + req.log.debug(`Existing member to application ${JSON.stringify()}`); + const m = members.find(m => m.userId === application.userId); + application.existingMembership = m; + return application; }); req.log.debug(`Enriched Applications ${JSON.stringify(enrichedApplications)}`); From 76a22c15cc80adc57a91bb3f82ad99ac46632e9e Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 14:37:39 +0200 Subject: [PATCH 084/105] added existing membership to the copilot application --- src/routes/copilotOpportunityApply/list.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index bfaa6109..30021e7c 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -57,14 +57,16 @@ module.exports = [ req.log.debug(`Fetched existing active members ${JSON.stringify(members)}`); req.log.debug(`Applications ${JSON.stringify(copilotApplications)}`); const enrichedApplications = copilotApplications.map(application => { - req.log.debug(`Existing member to application ${JSON.stringify()}`); const m = members.find(m => m.userId === application.userId); - application.existingMembership = m; - return application; + // req.log.debug(`Existing member to application ${JSON.stringify(m)}`); + return { + ...application, + existingMembership: m, + }; }); req.log.debug(`Enriched Applications ${JSON.stringify(enrichedApplications)}`); - return res.json(enrichedApplications); + res.status(200).send(enrichedApplications); }); }) }) From cf120a8c355b67865ff5e7b375acbb4de24b2edb Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 14:50:46 +0200 Subject: [PATCH 085/105] added existing membership to the copilot application --- src/routes/copilotOpportunityApply/list.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index 30021e7c..21d81454 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -58,11 +58,12 @@ module.exports = [ req.log.debug(`Applications ${JSON.stringify(copilotApplications)}`); const enrichedApplications = copilotApplications.map(application => { const m = members.find(m => m.userId === application.userId); - // req.log.debug(`Existing member to application ${JSON.stringify(m)}`); - return { - ...application, + req.log.debug(`Existing member to application ${JSON.stringify(Object.assign({}, application, { existingMembership: m, - }; + }))}`); + return Object.assign({}, application, { + existingMembership: m, + }); }); req.log.debug(`Enriched Applications ${JSON.stringify(enrichedApplications)}`); From 471d5e1c3abaa5f60c91157f4085efbee71571d0 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 15:04:53 +0200 Subject: [PATCH 086/105] added existing membership to the copilot application --- src/routes/copilotOpportunityApply/list.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index 21d81454..e0363507 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -61,9 +61,9 @@ module.exports = [ req.log.debug(`Existing member to application ${JSON.stringify(Object.assign({}, application, { existingMembership: m, }))}`); - return Object.assign({}, application, { + return Object.assign({}, application, m ? { existingMembership: m, - }); + } : {}); }); req.log.debug(`Enriched Applications ${JSON.stringify(enrichedApplications)}`); From 1559e311c3eb48adaa08841c07062a3f7abb6073 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 15:05:11 +0200 Subject: [PATCH 087/105] added existing membership to the copilot application --- src/routes/copilotOpportunityApply/list.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index e0363507..deeabd85 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -58,9 +58,9 @@ module.exports = [ req.log.debug(`Applications ${JSON.stringify(copilotApplications)}`); const enrichedApplications = copilotApplications.map(application => { const m = members.find(m => m.userId === application.userId); - req.log.debug(`Existing member to application ${JSON.stringify(Object.assign({}, application, { + req.log.debug(`Existing member to application ${JSON.stringify(Object.assign({}, application, m ? { existingMembership: m, - }))}`); + } : {}))}`); return Object.assign({}, application, m ? { existingMembership: m, } : {}); From 44ac8422dadefdc197b1cc9c6c6963ea68418c21 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 15:08:31 +0200 Subject: [PATCH 088/105] added existing membership to the copilot application --- src/routes/copilotOpportunityApply/list.js | 32 ++++++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index deeabd85..6051b027 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -58,12 +58,32 @@ module.exports = [ req.log.debug(`Applications ${JSON.stringify(copilotApplications)}`); const enrichedApplications = copilotApplications.map(application => { const m = members.find(m => m.userId === application.userId); - req.log.debug(`Existing member to application ${JSON.stringify(Object.assign({}, application, m ? { - existingMembership: m, - } : {}))}`); - return Object.assign({}, application, m ? { - existingMembership: m, - } : {}); + + // Using spread operator fails in lint check + // While Object.assign fails silently during run time + // So using this method + const enriched = { + id: application.id, + opportunityId: application.opportunityId, + notes: application.notes, + status: application.status, + userId: application.userId, + deletedAt: application.deletedAt, + createdAt: application.createdAt, + updatedAt: application.updatedAt, + deletedBy: application.deletedBy, + createdBy: application.createdBy, + updatedBy: application.updatedBy, + copilotOpportunity: application.copilotOpportunity, + }; + + if (m) { + enriched.existingMembership = m; + } + + req.log.debug(`Existing member to application ${JSON.stringify(enriched)}`); + + return enriched; }); req.log.debug(`Enriched Applications ${JSON.stringify(enrichedApplications)}`); From de8c14f6771c8292d666818e5b142a1a96498be3 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 15:46:00 +0200 Subject: [PATCH 089/105] added existing membership to the copilot application --- src/routes/copilotOpportunity/assign.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index f783a534..ef98189d 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -72,23 +72,27 @@ module.exports = [ const userId = application.userId; const activeMembers = await models.ProjectMember.getActiveProjectMembers(projectId, t); const updateCopilotOpportunity = async () => { + req.log.debug(`Updating opportunity: ${JSON.stringify(opportunity)}`); await opportunity.update({ status: COPILOT_OPPORTUNITY_STATUS.COMPLETED, }, { transaction: t, }); + req.log.debug(`Updating application: ${JSON.stringify(application)}`); await application.update({ status: COPILOT_APPLICATION_STATUS.ACCEPTED, }, { transaction: t, }); + req.log.debug(`Updating request: ${JSON.stringify(copilotRequest)}`); await copilotRequest.update({ status: COPILOT_REQUEST_STATUS.FULFILLED, }, { transaction: t, }); + req.log.debug(`Updating other applications: ${JSON.stringify(copilotRequest)}`); await models.CopilotApplication.update({ status: COPILOT_APPLICATION_STATUS.CANCELED, }, { @@ -100,6 +104,8 @@ module.exports = [ }, } }); + + req.log.debug(`All updations done`); }; const existingMember = activeMembers.find(item => item.userId === userId); @@ -135,6 +141,7 @@ module.exports = [ req.log.debug(`Member updated in kafka`); updateCopilotOpportunity(); } + t.commit(); res.status(200).send({ id: applicationId }); return; } From 40076d3e505e7915e917aab8ddee6407786c1a34 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 16:33:11 +0200 Subject: [PATCH 090/105] added existing membership to the copilot application --- src/routes/copilotOpportunity/assign.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index ef98189d..406c9e32 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -72,24 +72,25 @@ module.exports = [ const userId = application.userId; const activeMembers = await models.ProjectMember.getActiveProjectMembers(projectId, t); const updateCopilotOpportunity = async () => { + const transaction = await models.sequelize.transaction(); req.log.debug(`Updating opportunity: ${JSON.stringify(opportunity)}`); await opportunity.update({ status: COPILOT_OPPORTUNITY_STATUS.COMPLETED, }, { - transaction: t, + transaction, }); req.log.debug(`Updating application: ${JSON.stringify(application)}`); await application.update({ status: COPILOT_APPLICATION_STATUS.ACCEPTED, }, { - transaction: t, + transaction, }); req.log.debug(`Updating request: ${JSON.stringify(copilotRequest)}`); await copilotRequest.update({ status: COPILOT_REQUEST_STATUS.FULFILLED, }, { - transaction: t, + transaction, }); req.log.debug(`Updating other applications: ${JSON.stringify(copilotRequest)}`); @@ -106,6 +107,7 @@ module.exports = [ }); req.log.debug(`All updations done`); + transaction.commit(); }; const existingMember = activeMembers.find(item => item.userId === userId); @@ -141,7 +143,6 @@ module.exports = [ req.log.debug(`Member updated in kafka`); updateCopilotOpportunity(); } - t.commit(); res.status(200).send({ id: applicationId }); return; } From d40373d52468624ba5a6ff8fea2c22bf20287e44 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 16:55:36 +0200 Subject: [PATCH 091/105] added existing membership to the copilot application --- src/routes/copilotOpportunity/assign.js | 30 ++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index 406c9e32..98928b47 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -1,11 +1,13 @@ import _ from 'lodash'; import validate from 'express-validation'; import Joi from 'joi'; +import config from 'config'; import models from '../../models'; import util from '../../util'; import { PERMISSION } from '../../permissions/constants'; -import { COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, PROJECT_MEMBER_ROLE, RESOURCES } from '../../constants'; +import { CONNECT_NOTIFICATION_EVENT, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, PROJECT_MEMBER_ROLE, RESOURCES, TEMPLATE_IDS } from '../../constants'; +import { getCopilotTypeLabel } from '../../utils/copilot'; const assignCopilotOpportunityValidations = { body: Joi.object().keys({ @@ -73,6 +75,8 @@ module.exports = [ const activeMembers = await models.ProjectMember.getActiveProjectMembers(projectId, t); const updateCopilotOpportunity = async () => { const transaction = await models.sequelize.transaction(); + const memberDetails = await util.getMemberDetailsByUserIds([application.userId], req.log, req.id); + const member = memberDetails[0]; req.log.debug(`Updating opportunity: ${JSON.stringify(opportunity)}`); await opportunity.update({ status: COPILOT_OPPORTUNITY_STATUS.COMPLETED, @@ -108,6 +112,26 @@ module.exports = [ req.log.debug(`All updations done`); transaction.commit(); + + req.log.debug(`Sending email notification`); + const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL; + const copilotPortalUrl = config.get('copilotPortalUrl'); + const requestData = copilotRequest.data; + createEvent(emailEventType, { + data: { + opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`, + work_manager_url: config.get('workManagerUrl'), + opportunity_type: getCopilotTypeLabel(requestData.projectType), + opportunity_title: requestData.opportunityTitle, + start_date: moment.utc(requestData.startDate).format('DD-MM-YYYY'), + user_name: member ? member.handle : "", + }, + sendgrid_template_id: TEMPLATE_IDS.COPILOT_ALREADY_PART_OF_PROJECT, + recipients: [member.email], + version: 'v3', + }, req.log); + + req.log.debug(`Email sent`); }; const existingMember = activeMembers.find(item => item.userId === userId); @@ -115,7 +139,7 @@ module.exports = [ req.log.debug(`User already part of project: ${JSON.stringify(existingMember)}`); if (['copilot', 'manager'].includes(existingMember.role)) { req.log.debug(`User is a copilot or manager`); - updateCopilotOpportunity(); + await updateCopilotOpportunity(); } else { req.log.debug(`User has read/write role`); await models.ProjectMember.update({ @@ -141,7 +165,7 @@ module.exports = [ projectMember.get({ plain: true }), existingMember); req.log.debug(`Member updated in kafka`); - updateCopilotOpportunity(); + await updateCopilotOpportunity(); } res.status(200).send({ id: applicationId }); return; From 8b2fa3e74c4853f78a240a81a88cd750b2fe4c80 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 17:09:33 +0200 Subject: [PATCH 092/105] added existing membership to the copilot application --- src/routes/copilotOpportunity/assign.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index 98928b47..989425e3 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -102,7 +102,6 @@ module.exports = [ status: COPILOT_APPLICATION_STATUS.CANCELED, }, { where: { - projectId, opportunityId: opportunity.id, id: { $ne: application.id, From e7a328b488b63eb47bde4b9d3cb86aad01727b08 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 17:27:59 +0200 Subject: [PATCH 093/105] added existing membership to the copilot application --- src/routes/copilotOpportunity/assign.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index 989425e3..20e6df17 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -8,6 +8,7 @@ import util from '../../util'; import { PERMISSION } from '../../permissions/constants'; import { CONNECT_NOTIFICATION_EVENT, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, PROJECT_MEMBER_ROLE, RESOURCES, TEMPLATE_IDS } from '../../constants'; import { getCopilotTypeLabel } from '../../utils/copilot'; +import { createEvent } from '../../services/busApi'; const assignCopilotOpportunityValidations = { body: Joi.object().keys({ From ab268ca96a3445b7811e743aa6784fbd1e9e0cbe Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 17:38:44 +0200 Subject: [PATCH 094/105] added existing membership to the copilot application --- src/routes/projectMembers/update.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index e38b6e14..5204897b 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -263,7 +263,7 @@ module.exports = [ projectMember = projectMember.get({ plain: true }); projectMember = _.omit(projectMember, ['deletedAt']); - if (['observer', 'customer'].includes(updatedProps.role)) { + if (['observer', 'customer'].includes(previousValue.role)) { await completeAllCopilotRequests(req, projectId, _transaction, _member); } }) From e82e6b2fbbbb45c9510edded2ab9a4972010c0ae Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 17:42:20 +0200 Subject: [PATCH 095/105] added existing membership to the copilot application --- src/routes/copilotOpportunity/assign.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index 20e6df17..6cab3b55 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -9,6 +9,7 @@ import { PERMISSION } from '../../permissions/constants'; import { CONNECT_NOTIFICATION_EVENT, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, PROJECT_MEMBER_ROLE, RESOURCES, TEMPLATE_IDS } from '../../constants'; import { getCopilotTypeLabel } from '../../utils/copilot'; import { createEvent } from '../../services/busApi'; +import moment from 'moment'; const assignCopilotOpportunityValidations = { body: Joi.object().keys({ From dad92bc70a4c5fe091066588d6aedf51c2bed3ae Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 22:03:37 +0200 Subject: [PATCH 096/105] fix: show already member modal --- src/routes/projectMembers/update.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index 5204897b..acbc6505 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -264,7 +264,7 @@ module.exports = [ projectMember = _.omit(projectMember, ['deletedAt']); if (['observer', 'customer'].includes(previousValue.role)) { - await completeAllCopilotRequests(req, projectId, _transaction, _member); + await completeAllCopilotRequests(req, projectId, _transaction, projectMember); } }) .then(() => ( From ada10e8aefb0437b97f46a26ac427df26126aa56 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 22:07:02 +0200 Subject: [PATCH 097/105] fix: show already member modal --- src/routes/projectMembers/update.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index acbc6505..72ce1add 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -163,8 +163,6 @@ const completeAllCopilotRequests = async (req, projectId, _transaction, _member) req.log.debug(`Sent email to ${member.email}`); }); - - await _transaction.commit(); }; module.exports = [ From c2df9ee109a51a951c614ae5a4a8cedae933c4a9 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Jul 2025 22:09:22 +0200 Subject: [PATCH 098/105] fix: show already member modal --- src/routes/projectMembers/update.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index 72ce1add..882ddfdc 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -261,7 +261,7 @@ module.exports = [ projectMember = projectMember.get({ plain: true }); projectMember = _.omit(projectMember, ['deletedAt']); - if (['observer', 'customer'].includes(previousValue.role)) { + if (['observer', 'customer'].includes(previousValue.role) && ['copilot', 'manager'].includes(updatedProps.role)) { await completeAllCopilotRequests(req, projectId, _transaction, projectMember); } }) From 65d9a7e1ed73007f28104d336972fff8bea27386 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 31 Jul 2025 10:40:02 +0530 Subject: [PATCH 099/105] deploy branch, fix sorting script --- .circleci/config.yml | 2 +- src/routes/copilotOpportunity/list.js | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 268aff93..9bdd9263 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'pm-1510'] + only: ['develop', 'migration-setup', 'PM-1314'] - deployProd: context : org-global filters: diff --git a/src/routes/copilotOpportunity/list.js b/src/routes/copilotOpportunity/list.js index 2cc4c152..02154217 100644 --- a/src/routes/copilotOpportunity/list.js +++ b/src/routes/copilotOpportunity/list.js @@ -35,8 +35,15 @@ module.exports = [ }, ], order: [ - [models.Sequelize.literal(`CASE WHEN "CopilotOpportunity"."status" = 'active' THEN 0 ELSE 1 END`), 'ASC'], - [sortParams[0], sortParams[1]] + [models.Sequelize.literal(` + CASE + WHEN "CopilotOpportunity"."status" = 'active' THEN 0 + WHEN "CopilotOpportunity"."status" = 'cancelled' THEN 1 + WHEN "CopilotOpportunity"."status" = 'completed' THEN 2 + ELSE 3 + END + `), 'ASC'], + [sortParams[0], sortParams[1]], ], limit, offset, From 54bff16b3a6545ca83f226209103e7237b92da3a Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 31 Jul 2025 10:43:06 +0530 Subject: [PATCH 100/105] Deploy - PM-1314 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 268aff93..9bdd9263 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'pm-1510'] + only: ['develop', 'migration-setup', 'PM-1314'] - deployProd: context : org-global filters: From ab5e2f0e94436af246f66d2c704abf172c5a10a7 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 31 Jul 2025 12:53:02 +0530 Subject: [PATCH 101/105] PM-1314 Remove grouping logic --- src/routes/copilotOpportunity/list.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/routes/copilotOpportunity/list.js b/src/routes/copilotOpportunity/list.js index 02154217..6e90a4f7 100644 --- a/src/routes/copilotOpportunity/list.js +++ b/src/routes/copilotOpportunity/list.js @@ -35,14 +35,6 @@ module.exports = [ }, ], order: [ - [models.Sequelize.literal(` - CASE - WHEN "CopilotOpportunity"."status" = 'active' THEN 0 - WHEN "CopilotOpportunity"."status" = 'cancelled' THEN 1 - WHEN "CopilotOpportunity"."status" = 'completed' THEN 2 - ELSE 3 - END - `), 'ASC'], [sortParams[0], sortParams[1]], ], limit, From eb88f39446309e9495e6c7b27250e3b471b29949 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 31 Jul 2025 13:37:31 +0530 Subject: [PATCH 102/105] PM-1314 Make grouping optional --- src/routes/copilotOpportunity/list.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/routes/copilotOpportunity/list.js b/src/routes/copilotOpportunity/list.js index 6e90a4f7..9a806290 100644 --- a/src/routes/copilotOpportunity/list.js +++ b/src/routes/copilotOpportunity/list.js @@ -21,6 +21,25 @@ module.exports = [ const pageSize = parseInt(req.query.pageSize, 10) || DEFAULT_PAGE_SIZE; const offset = (page - 1) * pageSize; const limit = pageSize; + const noGroupingByStatus = req.query.noGrouping === 'true'; + + const baseOrder = []; + + // If grouping is enabled (default), add custom ordering based on status + if (!noGroupingByStatus) { + baseOrder.push([ + models.Sequelize.literal(` + CASE + WHEN "CopilotOpportunity"."status" = 'active' THEN 0 + WHEN "CopilotOpportunity"."status" = 'cancelled' THEN 1 + WHEN "CopilotOpportunity"."status" = 'completed' THEN 2 + ELSE 3 + END + `), + 'ASC', + ]); + } + baseOrder.push([sortParams[0], sortParams[1]]); return models.CopilotOpportunity.findAll({ include: [ @@ -34,9 +53,7 @@ module.exports = [ attributes: ['name'], }, ], - order: [ - [sortParams[0], sortParams[1]], - ], + order: baseOrder, limit, offset, }) From 808211279441c0c63ceaa0e06b50b12dbd4548dc Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 31 Jul 2025 20:01:10 +0200 Subject: [PATCH 103/105] fix: dont allow invite when opportunity is closed --- .../copilotRequest/approveRequest.service.js | 6 ++--- src/routes/projectMemberInvites/update.js | 27 ++++++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index 31e8db89..5ef4d2c1 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -4,7 +4,7 @@ import moment from 'moment'; import { Op } from 'sequelize'; import models from '../../models'; -import { CONNECT_NOTIFICATION_EVENT, COPILOT_REQUEST_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants'; +import { CONNECT_NOTIFICATION_EVENT, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants'; import util from '../../util'; import { createEvent } from '../../services/busApi'; import { getCopilotTypeLabel } from '../../utils/copilot'; @@ -46,13 +46,13 @@ module.exports = (req, data, existingTransaction) => { projectId, type: data.type, status: { - [Op.notIn]: [COPILOT_REQUEST_STATUS.CANCELED], + [Op.in]: [COPILOT_OPPORTUNITY_STATUS.ACTIVE], } }, }) .then((existingCopilotOpportunityOfSameType) => { if (existingCopilotOpportunityOfSameType) { - const err = new Error('There\'s an opportunity of same type already!'); + const err = new Error('There\'s an active opportunity of same type already!'); _.assign(err, { status: 403, }); diff --git a/src/routes/projectMemberInvites/update.js b/src/routes/projectMemberInvites/update.js index d7ff1d66..19414817 100644 --- a/src/routes/projectMemberInvites/update.js +++ b/src/routes/projectMemberInvites/update.js @@ -50,7 +50,7 @@ module.exports = [ // get invite by id and project id return models.ProjectMemberInvite.getPendingOrRequestedProjectInviteById(projectId, inviteId) - .then((invite) => { + .then(async (invite) => { // if invite doesn't exist, return 404 if (!invite) { const err = new Error(`invite not found for project id ${projectId}, inviteId ${inviteId},` + @@ -85,6 +85,31 @@ module.exports = [ return next(err); } + // Check if the copilot opportunity is still active + // When the invited user tries to accept the invite + if (invite.applicationId) { + req.log.debug(`Invite from copilot application: ${invite.applicationId}`); + const application = await models.CopilotApplication.findOne({ + where: { + id: invite.applicationId, + } + }); + + const opportunity = await models.CopilotOpportunity.findOne({ + where: { + id: application.opportunityId, + }, + }); + + req.log.debug(`Copilot opportunity status: ${opportunity.status}`); + if (opportunity.status !== COPILOT_OPPORTUNITY_STATUS.ACTIVE) { + req.log.debug(`Copilot opportunity status is not active`); + const err = new Error('The copilot opportunity is not in active status'); + err.status = 409; + return next(err); + } + } + req.log.debug('Updating invite status'); return invite .update({ From 82dee4ede3fd558badfef8f601d50fee9403e75c Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 31 Jul 2025 20:01:28 +0200 Subject: [PATCH 104/105] fix: dont allow invite when opportunity is closed --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9bdd9263..b3f2600c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'PM-1314'] + only: ['develop', 'migration-setup', 'pm-1398'] - deployProd: context : org-global filters: From b1d7b407d7b47271e7f85c2323137411b27cfb2c Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 1 Aug 2025 17:20:08 +0200 Subject: [PATCH 105/105] cancel invites on canceling the copilot opportunity --- src/routes/copilotOpportunity/delete.js | 27 ++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/routes/copilotOpportunity/delete.js b/src/routes/copilotOpportunity/delete.js index 3c6d9bfa..5336807f 100644 --- a/src/routes/copilotOpportunity/delete.js +++ b/src/routes/copilotOpportunity/delete.js @@ -1,10 +1,12 @@ import _ from 'lodash'; +import { Op } from 'sequelize'; import models from '../../models'; import util from '../../util'; -import { COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS } from '../../constants'; +import { COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, RESOURCES } from '../../constants'; import { PERMISSION } from '../../permissions/constants'; + module.exports = [ (req, res, next) => { if (!util.hasPermissionByReq(PERMISSION.CANCEL_COPILOT_OPPORTUNITY, req)) { @@ -54,6 +56,14 @@ module.exports = [ })); }); + const allInvites = await models.ProjectMemberInvite.findAll({ + where: { + applicationId: { + [Op.in]: applications.map(item => item.id), + }, + }, + }); + await Promise.all(promises); await copilotRequest.update({ @@ -68,6 +78,21 @@ module.exports = [ transaction, }); + // update all the existing invites which are + // associated to the copilot opportunity + // with cancel status + for (const invite of allInvites) { + await invite.update({ + status: INVITE_STATUS.CANCELED, + }); + await invite.reload(); + util.sendResourceToKafkaBus( + req, + EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_UPDATED, + RESOURCES.PROJECT_MEMBER_INVITE, + invite.toJSON()); + } + res.status(200).send({ id: opportunity.id }); })