Skip to content

Commit 3ad202c

Browse files
committed
Merge branch 'develop' of github.com:topcoder-platform/tc-project-service into develop
2 parents 56de058 + ada92eb commit 3ad202c

File tree

22 files changed

+893
-121
lines changed

22 files changed

+893
-121
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-1378']
152+
only: ['develop', 'migration-setup', 'PM-1612', 'fix-project-exposing']
153153
- deployProd:
154154
context : org-global
155155
filters:

config/custom-environment-variables.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"AUTH0_PROXY_SERVER_URL" : "AUTH0_PROXY_SERVER_URL",
5454
"connectUrl": "CONNECT_URL",
5555
"workManagerUrl": "WORK_MANAGER_URL",
56+
"copilotsSlackEmail": "COPILOTS_SLACK_EMAIL",
5657
"accountsAppUrl": "ACCOUNTS_APP_URL",
5758
"inviteEmailSubject": "INVITE_EMAIL_SUBJECT",
5859
"inviteEmailSectionTitle": "INVITE_EMAIL_SECTION_TITLE",

config/default.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"workManagerUrl": "https://challenges.topcoder-dev.com",
5757
"copilotPortalUrl": "https://copilots.topcoder-dev.com",
5858
"accountsAppUrl": "https://accounts.topcoder-dev.com",
59+
"copilotsSlackEmail": "[email protected]",
5960
"MAX_REVISION_NUMBER": 100,
6061
"UNIQUE_GMAIL_VALIDATION": false,
6162
"pageSize": 20,

config/production.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
"copilotPortalUrl": "https://copilots.topcoder.com",
55
"sfdcBillingAccountNameField": "Billing_Account_name__c",
66
"sfdcBillingAccountMarkupField": "Mark_up__c",
7-
"sfdcBillingAccountActiveField": "Active__c"
7+
"sfdcBillingAccountActiveField": "Active__c",
8+
"copilotsSlackEmail": "[email protected]"
89
}

src/constants.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,11 @@ 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',
316+
COPILOT_APPLICATION_ACCEPTED: 'd-eef5e7568c644940b250e76d026ced5b',
317+
COPILOT_OPPORTUNITY_COMPLETED: 'd-dc448919d11b4e7d8b4ba351c4b67b8b',
318+
COPILOT_OPPORTUNITY_CANCELED: 'd-2a67ba71e82f4d70891fe6989c3522a3'
314319
}
315320
export const REGEX = {
316321
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/models/copilotRequest.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import _ from 'lodash';
22
import { COPILOT_REQUEST_STATUS } from '../constants';
33

44
module.exports = function defineCopilotRequest(sequelize, DataTypes) {
5-
const CopliotRequest = sequelize.define('CopilotRequest', {
5+
const CopilotRequest = sequelize.define('CopilotRequest', {
66
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
77
status: {
88
type: DataTypes.STRING(16),
@@ -30,9 +30,10 @@ module.exports = function defineCopilotRequest(sequelize, DataTypes) {
3030
indexes: [],
3131
});
3232

33-
CopliotRequest.associate = (models) => {
34-
CopliotRequest.hasMany(models.CopilotOpportunity, { as: 'copilotOpportunity', foreignKey: 'copilotRequestId' });
33+
CopilotRequest.associate = (models) => {
34+
CopilotRequest.hasMany(models.CopilotOpportunity, { as: 'copilotOpportunity', foreignKey: 'copilotRequestId' });
35+
CopilotRequest.belongsTo(models.Project, { as: 'project', foreignKey: 'projectId' });
3536
};
3637

37-
return CopliotRequest;
38+
return CopilotRequest;
3839
};

src/routes/copilotOpportunity/assign.js

Lines changed: 197 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
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_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, PROJECT_MEMBER_ROLE, RESOURCES } from '../../constants';
9+
import { CONNECT_NOTIFICATION_EVENT, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, PROJECT_MEMBER_ROLE, RESOURCES, TEMPLATE_IDS, USER_ROLE } from '../../constants';
10+
import { getCopilotTypeLabel } from '../../utils/copilot';
11+
import { createEvent } from '../../services/busApi';
12+
import moment from 'moment';
13+
import { Op } from 'sequelize';
914

1015
const assignCopilotOpportunityValidations = {
1116
body: Joi.object().keys({
@@ -27,6 +32,34 @@ module.exports = [
2732
return next(err);
2833
}
2934

35+
const sendEmailToAllApplicants = async (copilotRequest, allApplications) => {
36+
37+
const userIds = allApplications.map(item => item.userId);
38+
39+
const users = await util.getMemberDetailsByUserIds(userIds, req.log, req.id);
40+
41+
users.forEach(async (user) => {
42+
req.log.debug(`Sending email notification to copilots who are not accepted`);
43+
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
44+
const copilotPortalUrl = config.get('copilotPortalUrl');
45+
const requestData = copilotRequest.data;
46+
createEvent(emailEventType, {
47+
data: {
48+
opportunity_details_url: copilotPortalUrl,
49+
work_manager_url: config.get('workManagerUrl'),
50+
opportunity_title: requestData.opportunityTitle,
51+
user_name: user ? user.handle : "",
52+
},
53+
sendgrid_template_id: TEMPLATE_IDS.COPILOT_OPPORTUNITY_COMPLETED,
54+
recipients: [user.email],
55+
version: 'v3',
56+
}, req.log);
57+
58+
req.log.debug(`Email sent to copilots who are not accepted`);
59+
});
60+
61+
};
62+
3063
return models.sequelize.transaction(async (t) => {
3164
const opportunity = await models.CopilotOpportunity.findOne({
3265
where: { id: copilotOpportunityId },
@@ -45,11 +78,17 @@ module.exports = [
4578
throw err;
4679
}
4780

81+
const copilotRequest = await models.CopilotRequest.findOne({
82+
where: { id: opportunity.copilotRequestId },
83+
transaction: t,
84+
});
85+
4886
const application = await models.CopilotApplication.findOne({
4987
where: { id: applicationId, opportunityId: copilotOpportunityId },
5088
transaction: t,
5189
});
5290

91+
5392
if (!application) {
5493
const err = new Error('No such application available');
5594
err.status = 400;
@@ -65,59 +104,179 @@ module.exports = [
65104
const projectId = opportunity.projectId;
66105
const userId = application.userId;
67106
const activeMembers = await models.ProjectMember.getActiveProjectMembers(projectId, t);
107+
const updateCopilotOpportunity = async () => {
108+
const transaction = await models.sequelize.transaction();
109+
const memberDetails = await util.getMemberDetailsByUserIds([application.userId], req.log, req.id);
110+
const member = memberDetails[0];
111+
req.log.debug(`Updating opportunity: ${JSON.stringify(opportunity)}`);
112+
await opportunity.update({
113+
status: COPILOT_OPPORTUNITY_STATUS.COMPLETED,
114+
}, {
115+
transaction,
116+
});
117+
req.log.debug(`Updating application: ${JSON.stringify(application)}`);
118+
await application.update({
119+
status: COPILOT_APPLICATION_STATUS.ACCEPTED,
120+
}, {
121+
transaction,
122+
});
68123

69-
const existingUser = activeMembers.find(item => item.userId === userId);
70-
if (existingUser && existingUser.role === 'copilot') {
71-
const err = new Error(`User is already a copilot of this project`);
72-
err.status = 400;
73-
throw err;
74-
}
124+
req.log.debug(`Updating request: ${JSON.stringify(copilotRequest)}`);
125+
await copilotRequest.update({
126+
status: COPILOT_REQUEST_STATUS.FULFILLED,
127+
}, {
128+
transaction,
129+
});
75130

76-
const existingInvite = await models.ProjectMemberInvite.findAll({
77-
where: {
78-
userId,
79-
projectId,
80-
role: PROJECT_MEMBER_ROLE.COPILOT,
81-
status: INVITE_STATUS.PENDING,
82-
},
83-
transaction: t,
84-
});
131+
req.log.debug(`Updating other applications: ${JSON.stringify(copilotRequest)}`);
132+
await models.CopilotApplication.update({
133+
status: COPILOT_APPLICATION_STATUS.CANCELED,
134+
}, {
135+
where: {
136+
opportunityId: opportunity.id,
137+
id: {
138+
$ne: application.id,
139+
},
140+
}
141+
});
85142

86-
if (existingInvite && existingInvite.length) {
87-
const err = new Error(`User already has an pending invite to the project`);
88-
err.status = 400;
89-
throw err;
143+
req.log.debug(`All updations done`);
144+
transaction.commit();
145+
146+
req.log.debug(`Sending email notification`);
147+
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
148+
const copilotPortalUrl = config.get('copilotPortalUrl');
149+
const requestData = copilotRequest.data;
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(`Email sent`);
165+
};
166+
167+
const existingMember = activeMembers.find(item => item.userId === userId);
168+
if (existingMember) {
169+
req.log.debug(`User already part of project: ${JSON.stringify(existingMember)}`);
170+
if (['copilot', 'manager'].includes(existingMember.role)) {
171+
req.log.debug(`User is a copilot or manager`);
172+
await updateCopilotOpportunity();
173+
} else {
174+
req.log.debug(`User has read/write role`);
175+
await models.ProjectMember.update({
176+
role: 'copilot',
177+
}, {
178+
where: {
179+
id: existingMember.id,
180+
},
181+
});
182+
183+
const projectMember = await models.ProjectMember.findOne({
184+
where: {
185+
id: existingMember.id,
186+
},
187+
});
188+
189+
req.log.debug(`Updated project member: ${JSON.stringify(projectMember.get({plain: true}))}`);
190+
191+
util.sendResourceToKafkaBus(
192+
req,
193+
EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED,
194+
RESOURCES.PROJECT_MEMBER,
195+
projectMember.get({ plain: true }),
196+
existingMember);
197+
req.log.debug(`Member updated in kafka`);
198+
await updateCopilotOpportunity();
199+
}
200+
res.status(200).send({ id: applicationId });
201+
return;
90202
}
91203

92-
const invite = await models.ProjectMemberInvite.create({
93-
status: INVITE_STATUS.PENDING,
94-
role: PROJECT_MEMBER_ROLE.COPILOT,
95-
userId,
204+
const member = {
96205
projectId,
97-
applicationId: application.id,
206+
role: USER_ROLE.TC_COPILOT,
207+
userId,
98208
createdBy: req.authUser.userId,
99-
createdAt: new Date(),
100209
updatedBy: req.authUser.userId,
101-
updatedAt: new Date(),
210+
};
211+
req.context = req.context || {};
212+
req.context.currentProjectMembers = activeMembers;
213+
await util.addUserToProject(req, member, t)
214+
215+
await application.update({
216+
status: COPILOT_APPLICATION_STATUS.ACCEPTED,
102217
}, {
103218
transaction: t,
104-
})
219+
});
105220

106-
util.sendResourceToKafkaBus(
107-
req,
108-
EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED,
109-
RESOURCES.PROJECT_MEMBER_INVITE,
110-
Object.assign({}, invite.toJSON(), {
111-
source: 'copilot_portal',
112-
}),
113-
);
221+
await opportunity.update({
222+
status: COPILOT_OPPORTUNITY_STATUS.COMPLETED,
223+
}, {
224+
transaction: t,
225+
});
114226

115-
await application.update({
116-
status: COPILOT_APPLICATION_STATUS.INVITED,
227+
228+
await copilotRequest.update({
229+
status: COPILOT_REQUEST_STATUS.FULFILLED,
117230
}, {
118231
transaction: t,
119232
});
120233

234+
const sendEmailToCopilot = async () => {
235+
const memberDetails = await util.getMemberDetailsByUserIds([application.userId], req.log, req.id);
236+
const member = memberDetails[0];
237+
req.log.debug(`Sending email notification to accepted copilot`);
238+
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
239+
const copilotPortalUrl = config.get('copilotPortalUrl');
240+
const requestData = copilotRequest.data;
241+
createEvent(emailEventType, {
242+
data: {
243+
opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`,
244+
opportunity_title: requestData.opportunityTitle,
245+
start_date: moment.utc(requestData.startDate).format('DD-MM-YYYY'),
246+
user_name: member ? member.handle : "",
247+
},
248+
sendgrid_template_id: TEMPLATE_IDS.COPILOT_APPLICATION_ACCEPTED,
249+
recipients: [member.email],
250+
version: 'v3',
251+
}, req.log);
252+
253+
req.log.debug(`Email sent to copilot`);
254+
};
255+
256+
await sendEmailToCopilot();
257+
258+
// Cancel other applications
259+
const otherApplications = await models.CopilotApplication.findAll({
260+
where: {
261+
opportunityId: copilotOpportunityId,
262+
id: {
263+
[Op.notIn]: [applicationId],
264+
},
265+
},
266+
transaction: t,
267+
});
268+
269+
// Send email to all applicants about opportunity completion
270+
await sendEmailToAllApplicants(copilotRequest, otherApplications);
271+
272+
for (const otherApplication of otherApplications) {
273+
await otherApplication.update({
274+
status: COPILOT_APPLICATION_STATUS.CANCELED,
275+
}, {
276+
transaction: t,
277+
});
278+
}
279+
121280
res.status(200).send({ id: applicationId });
122281
}).catch(err => next(err));
123282
},

0 commit comments

Comments
 (0)