From 25b8b346d5306115ad76b33b7430dd4159bc342d Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Jul 2025 21:54:09 +0200 Subject: [PATCH 01/42] 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 02/42] 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 03/42] 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 04/42] 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 05/42] 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 06/42] 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 07/42] 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 f26de03f1ea32fdc3e69607c72c56f0f8c66368b Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Jul 2025 22:01:19 +0200 Subject: [PATCH 08/42] 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 09/42] 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 10/42] 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 11/42] 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 12/42] 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 13/42] 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 14/42] 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 15/42] 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 16/42] 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 9385156b92bcb748a91578c245d0f302f4e07cc0 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 25 Jul 2025 17:17:37 +0200 Subject: [PATCH 17/42] 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 18/42] 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 19/42] 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 20/42] 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 21/42] 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 22/42] 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 23/42] 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 24/42] 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 25/42] 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 26/42] 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 27/42] 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 0d02782d4d04fc7b8a5d3968bff06c99411b5908 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Jul 2025 13:07:57 +0200 Subject: [PATCH 28/42] 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 29/42] 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 30/42] 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 31/42] 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 32/42] 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 33/42] 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 34/42] 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 35/42] 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 36/42] 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 37/42] 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 38/42] 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 39/42] 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 40/42] 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 41/42] 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 42/42] 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();