-
Notifications
You must be signed in to change notification settings - Fork 55
feat(PM-1510): Send Email notification to PM/creator once copilot accepts invite #839
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
25b8b34
7831f54
57a8d00
1b21d3b
fa13330
0536910
b67a6ba
f26de03
50d2dac
347caae
d4e4033
ecb836c
279d2f1
3998114
b7c5f95
bebe265
9385156
fdb09e3
2dc0ea2
13c8c39
f73f444
7fce661
1f2cba4
1266652
c701b1e
1d19d15
af842b0
0d02782
ce575c7
2f2e162
287e14f
21977ef
b4dbde5
fc0bffe
ccceb70
d56f233
37f740c
993ea57
b3e4f8e
45629b5
5025fa1
7075d3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
@@ -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,13 +323,39 @@ module.exports = [ | |
| const errorMessageForAlreadyMemberUser = 'User with such handle is already a member of the team.'; | ||
|
|
||
| if (inviteUserIds) { | ||
| // remove members already in the team | ||
| 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: { | ||
| [Op.in]: existingMembers.map(item => item.userId), | ||
| }, | ||
| projectId, | ||
| } | ||
| }); | ||
|
|
||
| req.log.debug(`Existing Project Members: ${JSON.stringify(projectMembers)}`); | ||
|
|
||
| const existingProjectMembersMap = projectMembers.reduce((acc, current) => { | ||
| return Object.assign({}, acc, { | ||
| [current.userId]: current, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using |
||
| }); | ||
| }, {}); | ||
|
|
||
| req.log.debug(`Existing Project Members Map: ${JSON.stringify(existingProjectMembersMap)}`); | ||
|
|
||
| _.remove(inviteUserIds, u => _.some(members, (m) => { | ||
| const isPresent = m.userId === u; | ||
| if (isPresent) { | ||
| failed.push(_.assign({}, { | ||
| handle: getUserHandleById(m.userId, inviteUsers), | ||
| message: errorMessageForAlreadyMemberUser, | ||
| error: "ALREADY_MEMBER", | ||
| role: existingProjectMembersMap[m.userId].role, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure that |
||
| })); | ||
| } | ||
| return isPresent; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,11 +2,15 @@ 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, USER_ROLE } from '../../constants'; | ||
| import { PERMISSION } from '../../permissions/constants'; | ||
| import { getCopilotTypeLabel } from '../../utils/copilot'; | ||
| import { createEvent } from '../../services/busApi'; | ||
|
|
||
|
|
||
| /** | ||
|
|
@@ -263,6 +267,68 @@ module.exports = [ | |
| }) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The removal of |
||
|
|
||
| if (source === 'copilot_portal' && invite.applicationId) { | ||
| const application = await models.CopilotApplication.findOne({ | ||
| where: { | ||
| id: invite.applicationId, | ||
| }, | ||
| }); | ||
|
|
||
| const opportunity = await models.CopilotOpportunity.findOne({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The variable |
||
| where: { | ||
| id: application.opportunityId, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a null check for |
||
| }, | ||
| include: [ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding error handling for the database query to ensure that any issues with fetching the |
||
| { | ||
| 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); | ||
|
|
||
| const creatorDetails = await util.getMemberDetailsByUserIds([opportunity.createdBy], req.log, req.id); | ||
| const inviteeDetails = await util.getMemberDetailsByUserIds([application.userId], req.log, req.id); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The variable |
||
| const creator = creatorDetails[0]; | ||
| const invitee = inviteeDetails[0]; | ||
| const listOfSubjects = subjects; | ||
| if (creator && creator.email) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The variable |
||
| const isCreatorPartofSubjects = subjects.find(item => { | ||
| if (!item.email) { | ||
| return false; | ||
| } | ||
|
|
||
| return item.email.toLowerCase() === creator.email.toLowerCase(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The variable There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The variable |
||
| }); | ||
| if (!isCreatorPartofSubjects) { | ||
| listOfSubjects.push({ | ||
| email: creator.email, | ||
| handle: creator.handle, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL; | ||
| const copilotPortalUrl = config.get('copilotPortalUrl'); | ||
| const requestData = opportunity.copilotRequest.data; | ||
| listOfSubjects.forEach((subject) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider using |
||
| 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); | ||
| }); | ||
| } | ||
|
|
||
| await t.commit(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding error handling for the |
||
| return res.json(util.postProcessInvites('$.email', updatedInvite, req)); | ||
| } catch (e) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,11 +2,17 @@ | |
| 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 } 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 { createEvent } from '../../services/busApi'; | ||
| import { getCopilotTypeLabel } from '../../utils/copilot'; | ||
|
|
||
|
|
||
| /** | ||
| * API to update a project member. | ||
|
|
@@ -27,12 +33,140 @@ const updateProjectMemberValdiations = { | |
| PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo in the variable name |
||
| PROJECT_MEMBER_ROLE.PROJECT_MANAGER, | ||
| ).required(), | ||
| action: Joi.string().allow('').optional(), | ||
| }), | ||
| query: { | ||
| fields: Joi.string().optional(), | ||
| }, | ||
| }; | ||
|
|
||
| const completeAllCopilotRequests = async (req, projectId, _transaction, _member) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| const allCopilotRequests = await models.CopilotRequest.findAll({ | ||
| where: { | ||
| projectId, | ||
| status: { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider checking if the |
||
| [Op.in]: [ | ||
| COPILOT_REQUEST_STATUS.APPROVED, | ||
| COPILOT_REQUEST_STATUS.NEW, | ||
| COPILOT_REQUEST_STATUS.SEEKING, | ||
| ], | ||
| } | ||
| }, | ||
| transaction: _transaction, | ||
| }); | ||
|
|
||
| req.log.debug(`all copilot requests ${JSON.stringify(allCopilotRequests)}`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The debug log message |
||
|
|
||
| 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`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The debug log message |
||
|
|
||
| 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)}`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The debug log message |
||
|
|
||
| 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`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The debug log message |
||
|
|
||
| const allCopilotApplications = await models.CopilotApplication.findAll({ | ||
| where: { | ||
| opportunityId: { | ||
| [Op.in]: copilotOpportunites.map(item => item.id), | ||
| }, | ||
| }, | ||
| 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)}`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The debug log message |
||
|
|
||
| await models.CopilotApplication.update({ | ||
| status: COPILOT_APPLICATION_STATUS.CANCELED, | ||
| }, { | ||
| where: { | ||
| id: { | ||
| [Op.in]: applicationsWithoutMemberApplication.map(item => item.id), | ||
| }, | ||
| }, | ||
| transaction: _transaction, | ||
| }); | ||
|
|
||
| // If the invited member | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding error handling for the case when |
||
| if (memberApplication) { | ||
| await models.CopilotApplication.update({ | ||
| status: COPILOT_APPLICATION_STATUS.ACCEPTED, | ||
| }, { | ||
| where: { | ||
| id: memberApplication.id, | ||
| }, | ||
| transaction: _transaction, | ||
| }); | ||
| } | ||
|
|
||
| req.log.debug(`updated all copilot applications`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The debug log message There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The log message |
||
|
|
||
| const memberDetails = await util.getMemberDetailsByUserIds([_member.userId], req.log, req.id); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider checking if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The removal of |
||
| const member = memberDetails[0]; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding error handling for the |
||
|
|
||
| req.log.debug(`member details: ${JSON.stringify(member)}`); | ||
|
|
||
| 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: ${JSON.stringify(requestData)}`); | ||
| const opportunity = copilotOpportunites.find(item => item.copilotRequestId === request.id); | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure |
||
| req.log.debug(`Opportunity: ${JSON.stringify(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: member ? member.handle : "", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a null check for |
||
| }, | ||
| sendgrid_template_id: TEMPLATE_IDS.COPILOT_ALREADY_PART_OF_PROJECT, | ||
| recipients: [member.email], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a null check for |
||
| version: 'v3', | ||
| }, req.log); | ||
|
|
||
| req.log.debug(`Sent email to ${member.email}`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a null check for |
||
| }); | ||
|
|
||
| await _transaction.commit(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding error handling around the |
||
| }; | ||
|
|
||
| module.exports = [ | ||
| // handles request validations | ||
| validate(updateProjectMemberValdiations), | ||
|
|
@@ -45,15 +179,16 @@ module.exports = [ | |
| let updatedProps = req.body; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider checking if |
||
| const projectId = _.parseInt(req.params.projectId); | ||
| const memberRecordId = _.parseInt(req.params.id); | ||
| const action = updatedProps.action; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The variable |
||
| updatedProps = _.pick(updatedProps, ['isPrimary', 'role']); | ||
| const fields = req.query.fields ? req.query.fields.split(',') : null; | ||
|
|
||
| let previousValue; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider using |
||
| // let newValue; | ||
| models.sequelize.transaction(() => models.ProjectMember.findOne({ | ||
| models.sequelize.transaction(async (_transaction) => models.ProjectMember.findOne({ | ||
| where: { id: memberRecordId, projectId }, | ||
| }) | ||
| .then((_member) => { | ||
| .then(async (_member) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider handling the promise rejection for |
||
| if (!_member) { | ||
| // handle 404 | ||
| const err = new Error(`project member not found for project id ${projectId} ` + | ||
|
|
@@ -76,10 +211,13 @@ module.exports = [ | |
| return Promise.reject(err); | ||
| } | ||
|
|
||
| req.log.debug(`updated props ${JSON.stringify(updatedProps)}`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider removing or adjusting the log level for this debug statement if it's not necessary for production, as excessive logging can impact performance and clutter logs. |
||
| req.log.debug(`previous values ${JSON.stringify(previousValue)}`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the previous line, evaluate the necessity of this debug log statement for production environments. |
||
| // no updates if no change | ||
| if (updatedProps.role === previousValue.role && | ||
| if ((updatedProps.role === previousValue.role || action === 'complete-copilot-requests') && | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The condition |
||
| (_.isUndefined(updatedProps.isPrimary) || | ||
| updatedProps.isPrimary === previousValue.isPrimary)) { | ||
| await completeAllCopilotRequests(req, projectId, _transaction, _member); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function |
||
| return Promise.resolve(); | ||
| } | ||
|
|
||
|
|
@@ -121,9 +259,13 @@ module.exports = [ | |
| }); | ||
| }) | ||
| .then(() => projectMember.reload(projectMember.id)) | ||
| .then(() => { | ||
| .then(async () => { | ||
| projectMember = projectMember.get({ plain: true }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider using |
||
| projectMember = _.omit(projectMember, ['deletedAt']); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function |
||
|
|
||
| if (['observer', 'customer'].includes(updatedProps.role)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The condition |
||
| await completeAllCopilotRequests(req, projectId, _transaction, _member); | ||
| } | ||
| }) | ||
| .then(() => ( | ||
| util.getObjectsWithMemberDetails([projectMember], fields, req) | ||
|
|
@@ -145,6 +287,9 @@ module.exports = [ | |
| req.log.debug('updated project member', projectMember); | ||
| res.json(memberWithDetails || projectMember); | ||
| }) | ||
| .catch(err => next(err))); | ||
| .catch(async (err) => { | ||
| await _transaction.rollback(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider checking the result of |
||
| return next(err); | ||
| })); | ||
| }, | ||
| ]; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
asyncin a.then()callback can lead to unexpected behavior and is generally not recommended. Consider refactoring this part of the code to useasync/awaitsyntax throughout for better readability and error handling.