diff --git a/.circleci/config.yml b/.circleci/config.yml index 1acd4a4c..5b58794d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup'] + only: ['develop', 'migration-setup', 'pm-1173'] - deployProd: context : org-global filters: diff --git a/config/default.json b/config/default.json index 43353496..d65b6383 100644 --- a/config/default.json +++ b/config/default.json @@ -54,6 +54,7 @@ "inviteEmailSubject": "You are invited to Topcoder", "inviteEmailSectionTitle": "Project Invitation", "workManagerUrl": "https://challenges.topcoder-dev.com/", + "copilotPortalUrl": "https://copilots.topcoder-dev.com/", "accountsAppUrl": "https://accounts.topcoder-dev.com/", "MAX_REVISION_NUMBER": 100, "UNIQUE_GMAIL_VALIDATION": false, diff --git a/config/development.json b/config/development.json index 5874ef2c..878bc669 100644 --- a/config/development.json +++ b/config/development.json @@ -3,6 +3,7 @@ "pubsubExchangeName": "dev.projects", "attachmentsS3Bucket": "topcoder-dev-media", "workManagerUrl": "https://challenges.topcoder-dev.com/", + "copilotPortalUrl": "https://copilots.topcoder-dev.com/", "fileServiceEndpoint": "https://api.topcoder-dev.com/v5/files", "memberServiceEndpoint": "https://api.topcoder-dev.com/v5/members", "identityServiceEndpoint": "https://api.topcoder-dev.com/v3/", diff --git a/config/production.json b/config/production.json index d784a55e..73399edf 100644 --- a/config/production.json +++ b/config/production.json @@ -1,6 +1,7 @@ { "authDomain": "topcoder.com", "workManagerUrl": "https://challenges.topcoder.com/", + "copilotPortalUrl": "https://copilots.topcoder.com/", "sfdcBillingAccountNameField": "Billing_Account_name__c", "sfdcBillingAccountMarkupField": "Mark_up__c", "sfdcBillingAccountActiveField": "Active__c" diff --git a/src/constants.js b/src/constants.js index ce2d90bb..02a8db53 100644 --- a/src/constants.js +++ b/src/constants.js @@ -302,8 +302,15 @@ export const CONNECT_NOTIFICATION_EVENT = { TOPIC_UPDATED: 'connect.notification.project.topic.updated', POST_CREATED: 'connect.notification.project.post.created', POST_UPDATED: 'connect.notification.project.post.edited', + + // External action email + EXTERNAL_ACTION_EMAIL: 'external.action.email', }; +export const TEMPLATE_IDS = { + APPLY_COPILOT: 'd-d7c1f48628654798a05c8e09e52db14f', + CREATE_REQUEST: 'd-3efdc91da580479d810c7acd50a4c17f', +} 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/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index 33daaddb..49e92c2f 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -1,11 +1,13 @@ import _ from 'lodash'; import validate from 'express-validation'; import Joi from 'joi'; +import config from 'config'; import models from '../../models'; import util from '../../util'; import { PERMISSION } from '../../permissions/constants'; -import { COPILOT_OPPORTUNITY_STATUS } from '../../constants'; +import { CONNECT_NOTIFICATION_EVENT, COPILOT_OPPORTUNITY_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants'; +import { createEvent } from '../../services/busApi'; const applyCopilotRequestValidations = { body: Joi.object().keys({ @@ -65,7 +67,36 @@ module.exports = [ } return models.CopilotApplication.create(data) - .then((result) => { + .then(async (result) => { + 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 creator = await util.getMemberDetailsByUserIds([opportunity.userId], req.log, req.id); + const listOfSubjects = subjects; + if (creator) { + const isCreatorPartofSubjects = subjects.find(item => item.email.toLowerCase() === creator[0].email.toLowerCase()); + if (!isCreatorPartofSubjects) { + listOfSubjects.push({ + email: creator[0].email, + handle: creator[0].handle, + }); + } + } + + const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL; + const copilotPortalUrl = config.get('copilotPortalUrl'); + listOfSubjects.forEach((subject) => { + createEvent(emailEventType, { + data: { + user_name: subject.handle, + opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}#applications`, + }, + sendgrid_template_id: TEMPLATE_IDS.APPLY_COPILOT, + recipients: [subject.email], + version: 'v3', + }, req.log); + }); + res.status(201).json(result); return Promise.resolve(); }) diff --git a/src/routes/copilotRequest/approveRequest.js b/src/routes/copilotRequest/approveRequest.js index 12fd6a8b..c1371671 100644 --- a/src/routes/copilotRequest/approveRequest.js +++ b/src/routes/copilotRequest/approveRequest.js @@ -35,7 +35,7 @@ module.exports = [ updatedBy: req.authUser.userId, }); - return approveRequest(data) + return approveRequest(req, data) .then(_newCopilotOpportunity => res.status(201).json(_newCopilotOpportunity)) .catch((err) => { if (err.message) { diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index b5164092..d4a2665d 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -1,7 +1,10 @@ import _ from 'lodash'; +import config from 'config'; import models from '../../models'; -import { COPILOT_REQUEST_STATUS } from '../../constants'; +import { CONNECT_NOTIFICATION_EVENT, COPILOT_REQUEST_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants'; +import util from '../../util'; +import { createEvent } from '../../services/busApi'; const resolveTransaction = (transaction, callback) => { if (transaction) { @@ -11,7 +14,7 @@ const resolveTransaction = (transaction, callback) => { return models.sequelize.transaction(callback); }; -module.exports = (data, existingTransaction) => { +module.exports = (req, data, existingTransaction) => { const { projectId, copilotRequestId } = data; return resolveTransaction(existingTransaction, transaction => @@ -52,6 +55,28 @@ module.exports = (data, existingTransaction) => { return models.CopilotOpportunity .create(data, { transaction }); })) + .then(async (opportunity) => { + const roles = await util.getRolesByRoleName(USER_ROLE.TC_COPILOT, req.log, req.id); + const { subjects = [] } = await util.getRoleInfo(roles[0], req.log, req.id); + const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL; + const copilotPortalUrl = config.get('copilotPortalUrl'); + req.log.info("Sending emails to all copilots about new opportunity"); + subjects.forEach(subject => { + createEvent(emailEventType, { + data: { + user_name: subject.handle, + opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`, + }, + sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST, + recipients: [subject.email], + version: 'v3', + }, req.log); + }); + + req.log.info("Finished sending emails to copilots"); + + return opportunity; + }) .catch((err) => { transaction.rollback(); return Promise.reject(err); diff --git a/src/routes/copilotRequest/create.js b/src/routes/copilotRequest/create.js index 95fa45f2..2b05f524 100644 --- a/src/routes/copilotRequest/create.js +++ b/src/routes/copilotRequest/create.js @@ -98,7 +98,7 @@ module.exports = [ updatedBy: req.authUser.userId, type: copilotRequest.data.projectType, }); - return approveRequest(approveData, transaction).then(() => copilotRequest); + return approveRequest(req, approveData, transaction).then(() => copilotRequest); }).then(copilotRequest => res.status(201).json(copilotRequest)) .catch((err) => { try { diff --git a/src/util.js b/src/util.js index 43b3c092..60c71d57 100644 --- a/src/util.js +++ b/src/util.js @@ -815,6 +815,54 @@ const projectServiceUtils = { } }, + getRoleInfo: Promise.coroutine(function* (roleId, logger, requestId) { // eslint-disable-line func-names + try { + const token = yield this.getM2MToken(); + const httpClient = this.getHttpClient({ id: requestId, log: logger }); + httpClient.defaults.timeout = 6000; + logger.debug(`${config.identityServiceEndpoint}roles/${roleId}`, "fetching role info"); + return httpClient.get(`${config.identityServiceEndpoint}roles/${roleId}`, { + params: { + fields: `subjects`, + }, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }).then((res) => { + logger.debug(`Role info by ${roleId}: ${JSON.stringify(res.data.result.content)}`); + return _.get(res, 'data.result.content', []); + }); + } catch (err) { + logger.debug(err, "error on getting role info"); + return Promise.reject(err); + } + }), + + getRolesByRoleName: Promise.coroutine(function* (roleName, logger, requestId) { // eslint-disable-line func-names + try { + const token = yield this.getM2MToken(); + const httpClient = this.getHttpClient({ id: requestId, log: logger }); + httpClient.defaults.timeout = 6000; + return httpClient.get(`${config.identityServiceEndpoint}roles`, { + params: { + filter: `roleName=${roleName}`, + }, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }).then((res) => { + logger.debug(`Roles by ${roleName}: ${JSON.stringify(res.data.result.content)}`); + return _.get(res, 'data.result.content', []) + .filter(item => item.roleName === roleName) + .map(r => r.id); + }); + } catch (err) { + return Promise.reject(err); + } + }), + /** * Retrieve member details from userIds */