diff --git a/.circleci/config.yml b/.circleci/config.yml index a0945e3b..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-1497'] + only: ['develop', 'migration-setup', 'pm-1510'] - deployProd: context : org-global filters: diff --git a/src/constants.js b/src/constants.js index bfc93281..1b727459 100644 --- a/src/constants.js +++ b/src/constants.js @@ -311,6 +311,8 @@ export const TEMPLATE_IDS = { APPLY_COPILOT: 'd-d7c1f48628654798a05c8e09e52db14f', 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/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 99f4334a..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 { @@ -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, + }); + }, {}); + + 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, })); } return isPresent; diff --git a/src/routes/projectMemberInvites/update.js b/src/routes/projectMemberInvites/update.js index 9f21b1c8..d7ff1d66 100644 --- a/src/routes/projectMemberInvites/update.js +++ b/src/routes/projectMemberInvites/update.js @@ -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 = [ }) } + 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); + + 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 => { + if (!item.email) { + return false; + } + + return item.email.toLowerCase() === creator.email.toLowerCase(); + }); + 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) => { + 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(); return res.json(util.postProcessInvites('$.email', updatedInvite, req)); } catch (e) { diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index 48ba6946..e38b6e14 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -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, PROJECT_MEMBER_ROLE.PROJECT_MANAGER, ).required(), + action: Joi.string().allow('').optional(), }), query: { fields: Joi.string().optional(), }, }; +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, + }); + + 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, + }); + + 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({ + status: COPILOT_APPLICATION_STATUS.CANCELED, + }, { + where: { + 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`); + + const memberDetails = await util.getMemberDetailsByUserIds([_member.userId], req.log, req.id); + const member = memberDetails[0]; + + 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); + + 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 : "", + }, + sendgrid_template_id: TEMPLATE_IDS.COPILOT_ALREADY_PART_OF_PROJECT, + recipients: [member.email], + version: 'v3', + }, req.log); + + req.log.debug(`Sent email to ${member.email}`); + }); + + await _transaction.commit(); +}; + module.exports = [ // handles request validations validate(updateProjectMemberValdiations), @@ -45,15 +179,16 @@ 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; let previousValue; // 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) => { 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)}`); + req.log.debug(`previous values ${JSON.stringify(previousValue)}`); // no updates if no change - if (updatedProps.role === previousValue.role && + if ((updatedProps.role === previousValue.role || action === 'complete-copilot-requests') && (_.isUndefined(updatedProps.isPrimary) || updatedProps.isPrimary === previousValue.isPrimary)) { + await completeAllCopilotRequests(req, projectId, _transaction, _member); return Promise.resolve(); } @@ -121,9 +259,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, _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(); + return next(err); + })); }, ];