Skip to content

Commit 6f4cb68

Browse files
authored
Merge pull request #815 from topcoder-platform/pm-1173
feat(PM-1173): Notify all copilots on copilot opportunity
2 parents 595e046 + e34f785 commit 6f4cb68

File tree

10 files changed

+121
-7
lines changed

10 files changed

+121
-7
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ workflows:
149149
context : org-global
150150
filters:
151151
branches:
152-
only: ['develop', 'migration-setup']
152+
only: ['develop', 'migration-setup', 'pm-1173']
153153
- deployProd:
154154
context : org-global
155155
filters:

config/default.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"inviteEmailSubject": "You are invited to Topcoder",
5555
"inviteEmailSectionTitle": "Project Invitation",
5656
"workManagerUrl": "https://challenges.topcoder-dev.com",
57+
"copilotPortalUrl": "https://copilots.topcoder-dev.com",
5758
"accountsAppUrl": "https://accounts.topcoder-dev.com",
5859
"MAX_REVISION_NUMBER": 100,
5960
"UNIQUE_GMAIL_VALIDATION": false,

config/development.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"pubsubExchangeName": "dev.projects",
44
"attachmentsS3Bucket": "topcoder-dev-media",
55
"workManagerUrl": "https://challenges.topcoder-dev.com",
6+
"copilotPortalUrl": "https://copilots.topcoder-dev.com",
67
"fileServiceEndpoint": "https://api.topcoder-dev.com/v5/files",
78
"memberServiceEndpoint": "https://api.topcoder-dev.com/v5/members",
89
"identityServiceEndpoint": "https://api.topcoder-dev.com/v3/",

config/production.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"authDomain": "topcoder.com",
33
"workManagerUrl": "https://challenges.topcoder.com",
4+
"copilotPortalUrl": "https://copilots.topcoder.com",
45
"sfdcBillingAccountNameField": "Billing_Account_name__c",
56
"sfdcBillingAccountMarkupField": "Mark_up__c",
67
"sfdcBillingAccountActiveField": "Active__c"

src/constants.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,15 @@ export const CONNECT_NOTIFICATION_EVENT = {
302302
TOPIC_UPDATED: 'connect.notification.project.topic.updated',
303303
POST_CREATED: 'connect.notification.project.post.created',
304304
POST_UPDATED: 'connect.notification.project.post.edited',
305+
306+
// External action email
307+
EXTERNAL_ACTION_EMAIL: 'external.action.email',
305308
};
306309

310+
export const TEMPLATE_IDS = {
311+
APPLY_COPILOT: 'd-d7c1f48628654798a05c8e09e52db14f',
312+
CREATE_REQUEST: 'd-3efdc91da580479d810c7acd50a4c17f',
313+
}
307314
export const REGEX = {
308315
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
309316
};

src/routes/copilotOpportunityApply/create.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import _ from 'lodash';
22
import validate from 'express-validation';
33
import Joi from 'joi';
4+
import config from 'config';
45

56
import models from '../../models';
67
import util from '../../util';
78
import { PERMISSION } from '../../permissions/constants';
8-
import { COPILOT_OPPORTUNITY_STATUS } from '../../constants';
9+
import { CONNECT_NOTIFICATION_EVENT, COPILOT_OPPORTUNITY_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants';
10+
import { createEvent } from '../../services/busApi';
911

1012
const applyCopilotRequestValidations = {
1113
body: Joi.object().keys({
@@ -65,7 +67,36 @@ module.exports = [
6567
}
6668

6769
return models.CopilotApplication.create(data)
68-
.then((result) => {
70+
.then(async (result) => {
71+
const pmRole = await util.getRolesByRoleName(USER_ROLE.PROJECT_MANAGER, req.log, req.id);
72+
const { subjects = [] } = await util.getRoleInfo(pmRole[0], req.log, req.id);
73+
74+
const creator = await util.getMemberDetailsByUserIds([opportunity.userId], req.log, req.id);
75+
const listOfSubjects = subjects;
76+
if (creator) {
77+
const isCreatorPartofSubjects = subjects.find(item => item.email.toLowerCase() === creator[0].email.toLowerCase());
78+
if (!isCreatorPartofSubjects) {
79+
listOfSubjects.push({
80+
email: creator[0].email,
81+
handle: creator[0].handle,
82+
});
83+
}
84+
}
85+
86+
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
87+
const copilotPortalUrl = config.get('copilotPortalUrl');
88+
listOfSubjects.forEach((subject) => {
89+
createEvent(emailEventType, {
90+
data: {
91+
user_name: subject.handle,
92+
opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}#applications`,
93+
},
94+
sendgrid_template_id: TEMPLATE_IDS.APPLY_COPILOT,
95+
recipients: [subject.email],
96+
version: 'v3',
97+
}, req.log);
98+
});
99+
69100
res.status(201).json(result);
70101
return Promise.resolve();
71102
})

src/routes/copilotRequest/approveRequest.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ module.exports = [
3535
updatedBy: req.authUser.userId,
3636
});
3737

38-
return approveRequest(data)
38+
return approveRequest(req, data)
3939
.then(_newCopilotOpportunity => res.status(201).json(_newCopilotOpportunity))
4040
.catch((err) => {
4141
if (err.message) {

src/routes/copilotRequest/approveRequest.service.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import _ from 'lodash';
2+
import config from 'config';
23

34
import models from '../../models';
4-
import { COPILOT_REQUEST_STATUS } from '../../constants';
5+
import { CONNECT_NOTIFICATION_EVENT, COPILOT_REQUEST_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants';
6+
import util from '../../util';
7+
import { createEvent } from '../../services/busApi';
58

69
const resolveTransaction = (transaction, callback) => {
710
if (transaction) {
@@ -11,7 +14,7 @@ const resolveTransaction = (transaction, callback) => {
1114
return models.sequelize.transaction(callback);
1215
};
1316

14-
module.exports = (data, existingTransaction) => {
17+
module.exports = (req, data, existingTransaction) => {
1518
const { projectId, copilotRequestId } = data;
1619

1720
return resolveTransaction(existingTransaction, transaction =>
@@ -52,6 +55,28 @@ module.exports = (data, existingTransaction) => {
5255
return models.CopilotOpportunity
5356
.create(data, { transaction });
5457
}))
58+
.then(async (opportunity) => {
59+
const roles = await util.getRolesByRoleName(USER_ROLE.TC_COPILOT, req.log, req.id);
60+
const { subjects = [] } = await util.getRoleInfo(roles[0], req.log, req.id);
61+
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
62+
const copilotPortalUrl = config.get('copilotPortalUrl');
63+
req.log.info("Sending emails to all copilots about new opportunity");
64+
subjects.forEach(subject => {
65+
createEvent(emailEventType, {
66+
data: {
67+
user_name: subject.handle,
68+
opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`,
69+
},
70+
sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST,
71+
recipients: [subject.email],
72+
version: 'v3',
73+
}, req.log);
74+
});
75+
76+
req.log.info("Finished sending emails to copilots");
77+
78+
return opportunity;
79+
})
5580
.catch((err) => {
5681
transaction.rollback();
5782
return Promise.reject(err);

src/routes/copilotRequest/create.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ module.exports = [
9898
updatedBy: req.authUser.userId,
9999
type: copilotRequest.data.projectType,
100100
});
101-
return approveRequest(approveData, transaction).then(() => copilotRequest);
101+
return approveRequest(req, approveData, transaction).then(() => copilotRequest);
102102
}).then(copilotRequest => res.status(201).json(copilotRequest))
103103
.catch((err) => {
104104
try {

src/util.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,54 @@ const projectServiceUtils = {
815815
}
816816
},
817817

818+
getRoleInfo: Promise.coroutine(function* (roleId, logger, requestId) { // eslint-disable-line func-names
819+
try {
820+
const token = yield this.getM2MToken();
821+
const httpClient = this.getHttpClient({ id: requestId, log: logger });
822+
httpClient.defaults.timeout = 6000;
823+
logger.debug(`${config.identityServiceEndpoint}roles/${roleId}`, "fetching role info");
824+
return httpClient.get(`${config.identityServiceEndpoint}roles/${roleId}`, {
825+
params: {
826+
fields: `subjects`,
827+
},
828+
headers: {
829+
'Content-Type': 'application/json',
830+
Authorization: `Bearer ${token}`,
831+
},
832+
}).then((res) => {
833+
logger.debug(`Role info by ${roleId}: ${JSON.stringify(res.data.result.content)}`);
834+
return _.get(res, 'data.result.content', []);
835+
});
836+
} catch (err) {
837+
logger.debug(err, "error on getting role info");
838+
return Promise.reject(err);
839+
}
840+
}),
841+
842+
getRolesByRoleName: Promise.coroutine(function* (roleName, logger, requestId) { // eslint-disable-line func-names
843+
try {
844+
const token = yield this.getM2MToken();
845+
const httpClient = this.getHttpClient({ id: requestId, log: logger });
846+
httpClient.defaults.timeout = 6000;
847+
return httpClient.get(`${config.identityServiceEndpoint}roles`, {
848+
params: {
849+
filter: `roleName=${roleName}`,
850+
},
851+
headers: {
852+
'Content-Type': 'application/json',
853+
Authorization: `Bearer ${token}`,
854+
},
855+
}).then((res) => {
856+
logger.debug(`Roles by ${roleName}: ${JSON.stringify(res.data.result.content)}`);
857+
return _.get(res, 'data.result.content', [])
858+
.filter(item => item.roleName === roleName)
859+
.map(r => r.id);
860+
});
861+
} catch (err) {
862+
return Promise.reject(err);
863+
}
864+
}),
865+
818866
/**
819867
* Retrieve member details from userIds
820868
*/

0 commit comments

Comments
 (0)