Skip to content

Commit 4a2a671

Browse files
authored
Merge pull request #839 from topcoder-platform/pm-1510
feat(PM-1510): Send Email notification to PM/creator once copilot accepts invite
2 parents 68522e0 + 7075d3f commit 4a2a671

File tree

4 files changed

+142
-11
lines changed

4 files changed

+142
-11
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', 'pm-1506']
152+
only: ['develop', 'migration-setup', 'pm-1510']
153153
- deployProd:
154154
context : org-global
155155
filters:

src/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,8 @@ export const TEMPLATE_IDS = {
311311
APPLY_COPILOT: 'd-d7c1f48628654798a05c8e09e52db14f',
312312
CREATE_REQUEST: 'd-3efdc91da580479d810c7acd50a4c17f',
313313
PROJECT_MEMBER_INVITED: 'd-b47a25b103604bc28fc0ce77e77fb681',
314+
INFORM_PM_COPILOT_APPLICATION_ACCEPTED: 'd-b35d073e302b4279a1bd208fcfe96f58',
315+
COPILOT_ALREADY_PART_OF_PROJECT: 'd-003d41cdc9de4bbc9e14538e8f2e0585',
314316
}
315317
export const REGEX = {
316318
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

src/routes/projectMemberInvites/update.js

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import validate from 'express-validation';
22
import _ from 'lodash';
33
import Joi from 'joi';
44
import { Op } from 'sequelize';
5+
import config from 'config';
6+
57
import { middleware as tcMiddleware } from 'tc-core-library-js';
68
import models from '../../models';
79
import util from '../../util';
8-
import { INVITE_STATUS, EVENT, RESOURCES, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, INVITE_SOURCE } from '../../constants';
10+
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';
911
import { PERMISSION } from '../../permissions/constants';
12+
import { getCopilotTypeLabel } from '../../utils/copilot';
13+
import { createEvent } from '../../services/busApi';
1014

1115

1216
/**
@@ -263,6 +267,68 @@ module.exports = [
263267
})
264268
}
265269

270+
if (source === 'copilot_portal' && invite.applicationId) {
271+
const application = await models.CopilotApplication.findOne({
272+
where: {
273+
id: invite.applicationId,
274+
},
275+
});
276+
277+
const opportunity = await models.CopilotOpportunity.findOne({
278+
where: {
279+
id: application.opportunityId,
280+
},
281+
include: [
282+
{
283+
model: models.CopilotRequest,
284+
as: 'copilotRequest',
285+
},
286+
],
287+
});
288+
const pmRole = await util.getRolesByRoleName(USER_ROLE.PROJECT_MANAGER, req.log, req.id);
289+
const { subjects = [] } = await util.getRoleInfo(pmRole[0], req.log, req.id);
290+
291+
const creatorDetails = await util.getMemberDetailsByUserIds([opportunity.createdBy], req.log, req.id);
292+
const inviteeDetails = await util.getMemberDetailsByUserIds([application.userId], req.log, req.id);
293+
const creator = creatorDetails[0];
294+
const invitee = inviteeDetails[0];
295+
const listOfSubjects = subjects;
296+
if (creator && creator.email) {
297+
const isCreatorPartofSubjects = subjects.find(item => {
298+
if (!item.email) {
299+
return false;
300+
}
301+
302+
return item.email.toLowerCase() === creator.email.toLowerCase();
303+
});
304+
if (!isCreatorPartofSubjects) {
305+
listOfSubjects.push({
306+
email: creator.email,
307+
handle: creator.handle,
308+
});
309+
}
310+
}
311+
312+
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
313+
const copilotPortalUrl = config.get('copilotPortalUrl');
314+
const requestData = opportunity.copilotRequest.data;
315+
listOfSubjects.forEach((subject) => {
316+
createEvent(emailEventType, {
317+
data: {
318+
user_name: subject.handle,
319+
opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}#applications`,
320+
work_manager_url: config.get('workManagerUrl'),
321+
opportunity_type: getCopilotTypeLabel(requestData.projectType),
322+
opportunity_title: requestData.opportunityTitle,
323+
copilot_handle: invitee ? invitee.handle : "",
324+
},
325+
sendgrid_template_id: TEMPLATE_IDS.INFORM_PM_COPILOT_APPLICATION_ACCEPTED,
326+
recipients: [subject.email],
327+
version: 'v3',
328+
}, req.log);
329+
});
330+
}
331+
266332
await t.commit();
267333
return res.json(util.postProcessInvites('$.email', updatedInvite, req));
268334
} catch (e) {

src/routes/projectMembers/update.js

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22
import validate from 'express-validation';
33
import _ from 'lodash';
44
import Joi from 'joi';
5+
import config from 'config';
6+
import moment from 'moment';
7+
import { Op } from 'sequelize';
58
import { middleware as tcMiddleware } from 'tc-core-library-js';
69
import models from '../../models';
710
import util from '../../util';
8-
import { EVENT, RESOURCES, PROJECT_MEMBER_ROLE, COPILOT_REQUEST_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_APPLICATION_STATUS } from '../../constants';
11+
import { EVENT, RESOURCES, PROJECT_MEMBER_ROLE, COPILOT_REQUEST_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_APPLICATION_STATUS, USER_ROLE, CONNECT_NOTIFICATION_EVENT, TEMPLATE_IDS } from '../../constants';
912
import { PERMISSION, PROJECT_TO_TOPCODER_ROLES_MATRIX } from '../../permissions/constants';
10-
import { Op } from 'sequelize';
13+
import { createEvent } from '../../services/busApi';
14+
import { getCopilotTypeLabel } from '../../utils/copilot';
15+
1116

1217
/**
1318
* API to update a project member.
@@ -28,17 +33,24 @@ const updateProjectMemberValdiations = {
2833
PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT,
2934
PROJECT_MEMBER_ROLE.PROJECT_MANAGER,
3035
).required(),
31-
action: Joi.string().optional(),
36+
action: Joi.string().allow('').optional(),
3237
}),
3338
query: {
3439
fields: Joi.string().optional(),
3540
},
3641
};
3742

38-
const completeAllCopilotRequests = async (req, projectId, _transaction) => {
43+
const completeAllCopilotRequests = async (req, projectId, _transaction, _member) => {
3944
const allCopilotRequests = await models.CopilotRequest.findAll({
4045
where: {
4146
projectId,
47+
status: {
48+
[Op.in]: [
49+
COPILOT_REQUEST_STATUS.APPROVED,
50+
COPILOT_REQUEST_STATUS.NEW,
51+
COPILOT_REQUEST_STATUS.SEEKING,
52+
],
53+
}
4254
},
4355
transaction: _transaction,
4456
});
@@ -91,20 +103,68 @@ const completeAllCopilotRequests = async (req, projectId, _transaction) => {
91103
transaction: _transaction,
92104
});
93105

106+
const memberApplication = allCopilotApplications.find(app => app.userId === _member.userId);
107+
const applicationsWithoutMemberApplication = allCopilotApplications.filter(app => app.userId !== _member.userId);
108+
94109
req.log.debug(`all copilot applications ${JSON.stringify(allCopilotApplications)}`);
95110

96111
await models.CopilotApplication.update({
97112
status: COPILOT_APPLICATION_STATUS.CANCELED,
98113
}, {
99114
where: {
100115
id: {
101-
[Op.in]: allCopilotApplications.map(item => item.id),
116+
[Op.in]: applicationsWithoutMemberApplication.map(item => item.id),
102117
},
103118
},
104119
transaction: _transaction,
105120
});
106121

122+
// If the invited member
123+
if (memberApplication) {
124+
await models.CopilotApplication.update({
125+
status: COPILOT_APPLICATION_STATUS.ACCEPTED,
126+
}, {
127+
where: {
128+
id: memberApplication.id,
129+
},
130+
transaction: _transaction,
131+
});
132+
}
133+
107134
req.log.debug(`updated all copilot applications`);
135+
136+
const memberDetails = await util.getMemberDetailsByUserIds([_member.userId], req.log, req.id);
137+
const member = memberDetails[0];
138+
139+
req.log.debug(`member details: ${JSON.stringify(member)}`);
140+
141+
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
142+
const copilotPortalUrl = config.get('copilotPortalUrl');
143+
allCopilotRequests.forEach((request) => {
144+
const requestData = request.data;
145+
146+
req.log.debug(`Copilot request data: ${JSON.stringify(requestData)}`);
147+
const opportunity = copilotOpportunites.find(item => item.copilotRequestId === request.id);
148+
149+
req.log.debug(`Opportunity: ${JSON.stringify(opportunity)}`);
150+
createEvent(emailEventType, {
151+
data: {
152+
opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`,
153+
work_manager_url: config.get('workManagerUrl'),
154+
opportunity_type: getCopilotTypeLabel(requestData.projectType),
155+
opportunity_title: requestData.opportunityTitle,
156+
start_date: moment.utc(requestData.startDate).format('DD-MM-YYYY'),
157+
user_name: member ? member.handle : "",
158+
},
159+
sendgrid_template_id: TEMPLATE_IDS.COPILOT_ALREADY_PART_OF_PROJECT,
160+
recipients: [member.email],
161+
version: 'v3',
162+
}, req.log);
163+
164+
req.log.debug(`Sent email to ${member.email}`);
165+
});
166+
167+
await _transaction.commit();
108168
};
109169

110170
module.exports = [
@@ -125,7 +185,7 @@ module.exports = [
125185

126186
let previousValue;
127187
// let newValue;
128-
models.sequelize.transaction((_transaction) => models.ProjectMember.findOne({
188+
models.sequelize.transaction(async (_transaction) => models.ProjectMember.findOne({
129189
where: { id: memberRecordId, projectId },
130190
})
131191
.then(async (_member) => {
@@ -157,7 +217,7 @@ module.exports = [
157217
if ((updatedProps.role === previousValue.role || action === 'complete-copilot-requests') &&
158218
(_.isUndefined(updatedProps.isPrimary) ||
159219
updatedProps.isPrimary === previousValue.isPrimary)) {
160-
await completeAllCopilotRequests(req, projectId, _transaction);
220+
await completeAllCopilotRequests(req, projectId, _transaction, _member);
161221
return Promise.resolve();
162222
}
163223

@@ -204,7 +264,7 @@ module.exports = [
204264
projectMember = _.omit(projectMember, ['deletedAt']);
205265

206266
if (['observer', 'customer'].includes(updatedProps.role)) {
207-
await completeAllCopilotRequests(req, projectId, _transaction);
267+
await completeAllCopilotRequests(req, projectId, _transaction, _member);
208268
}
209269
})
210270
.then(() => (
@@ -227,6 +287,9 @@ module.exports = [
227287
req.log.debug('updated project member', projectMember);
228288
res.json(memberWithDetails || projectMember);
229289
})
230-
.catch(err => next(err)));
290+
.catch(async (err) => {
291+
await _transaction.rollback();
292+
return next(err);
293+
}));
231294
},
232295
];

0 commit comments

Comments
 (0)