Skip to content

Commit 04409d3

Browse files
authored
Merge pull request #838 from topcoder-platform/develop
[PROD] - Copilot Portal fixes and updates
2 parents 86b9c3a + b9f8e59 commit 04409d3

File tree

19 files changed

+668
-59
lines changed

19 files changed

+668
-59
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-1398']
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": "aaaaplg3e4y6ccn3qniivvnxva@topcoder.slack.com",
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": "mem-ops-copilot-aaaadbvbjvvek6ojnwpfdh5qq4@topcoder.slack.com"
89
}

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/copilotOpportunity/assign.js

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
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 } from '../../constants';
10+
import { getCopilotTypeLabel } from '../../utils/copilot';
11+
import { createEvent } from '../../services/busApi';
12+
import moment from 'moment';
913

1014
const assignCopilotOpportunityValidations = {
1115
body: Joi.object().keys({
@@ -45,11 +49,17 @@ module.exports = [
4549
throw err;
4650
}
4751

52+
const copilotRequest = await models.CopilotRequest.findOne({
53+
where: { id: opportunity.copilotRequestId },
54+
transaction: t,
55+
});
56+
4857
const application = await models.CopilotApplication.findOne({
4958
where: { id: applicationId, opportunityId: copilotOpportunityId },
5059
transaction: t,
5160
});
5261

62+
5363
if (!application) {
5464
const err = new Error('No such application available');
5565
err.status = 400;
@@ -65,12 +75,101 @@ module.exports = [
6575
const projectId = opportunity.projectId;
6676
const userId = application.userId;
6777
const activeMembers = await models.ProjectMember.getActiveProjectMembers(projectId, t);
68-
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;
78+
const updateCopilotOpportunity = async () => {
79+
const transaction = await models.sequelize.transaction();
80+
const memberDetails = await util.getMemberDetailsByUserIds([application.userId], req.log, req.id);
81+
const member = memberDetails[0];
82+
req.log.debug(`Updating opportunity: ${JSON.stringify(opportunity)}`);
83+
await opportunity.update({
84+
status: COPILOT_OPPORTUNITY_STATUS.COMPLETED,
85+
}, {
86+
transaction,
87+
});
88+
req.log.debug(`Updating application: ${JSON.stringify(application)}`);
89+
await application.update({
90+
status: COPILOT_APPLICATION_STATUS.ACCEPTED,
91+
}, {
92+
transaction,
93+
});
94+
95+
req.log.debug(`Updating request: ${JSON.stringify(copilotRequest)}`);
96+
await copilotRequest.update({
97+
status: COPILOT_REQUEST_STATUS.FULFILLED,
98+
}, {
99+
transaction,
100+
});
101+
102+
req.log.debug(`Updating other applications: ${JSON.stringify(copilotRequest)}`);
103+
await models.CopilotApplication.update({
104+
status: COPILOT_APPLICATION_STATUS.CANCELED,
105+
}, {
106+
where: {
107+
opportunityId: opportunity.id,
108+
id: {
109+
$ne: application.id,
110+
},
111+
}
112+
});
113+
114+
req.log.debug(`All updations done`);
115+
transaction.commit();
116+
117+
req.log.debug(`Sending email notification`);
118+
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
119+
const copilotPortalUrl = config.get('copilotPortalUrl');
120+
const requestData = copilotRequest.data;
121+
createEvent(emailEventType, {
122+
data: {
123+
opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`,
124+
work_manager_url: config.get('workManagerUrl'),
125+
opportunity_type: getCopilotTypeLabel(requestData.projectType),
126+
opportunity_title: requestData.opportunityTitle,
127+
start_date: moment.utc(requestData.startDate).format('DD-MM-YYYY'),
128+
user_name: member ? member.handle : "",
129+
},
130+
sendgrid_template_id: TEMPLATE_IDS.COPILOT_ALREADY_PART_OF_PROJECT,
131+
recipients: [member.email],
132+
version: 'v3',
133+
}, req.log);
134+
135+
req.log.debug(`Email sent`);
136+
};
137+
138+
const existingMember = activeMembers.find(item => item.userId === userId);
139+
if (existingMember) {
140+
req.log.debug(`User already part of project: ${JSON.stringify(existingMember)}`);
141+
if (['copilot', 'manager'].includes(existingMember.role)) {
142+
req.log.debug(`User is a copilot or manager`);
143+
await updateCopilotOpportunity();
144+
} else {
145+
req.log.debug(`User has read/write role`);
146+
await models.ProjectMember.update({
147+
role: 'copilot',
148+
}, {
149+
where: {
150+
id: existingMember.id,
151+
},
152+
});
153+
154+
const projectMember = await models.ProjectMember.findOne({
155+
where: {
156+
id: existingMember.id,
157+
},
158+
});
159+
160+
req.log.debug(`Updated project member: ${JSON.stringify(projectMember.get({plain: true}))}`);
161+
162+
util.sendResourceToKafkaBus(
163+
req,
164+
EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED,
165+
RESOURCES.PROJECT_MEMBER,
166+
projectMember.get({ plain: true }),
167+
existingMember);
168+
req.log.debug(`Member updated in kafka`);
169+
await updateCopilotOpportunity();
170+
}
171+
res.status(200).send({ id: applicationId });
172+
return;
74173
}
75174

76175
const existingInvite = await models.ProjectMemberInvite.findAll({

src/routes/copilotOpportunity/delete.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import _ from 'lodash';
2+
import { Op } from 'sequelize';
23

34
import models from '../../models';
45
import util from '../../util';
5-
import { COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS } from '../../constants';
6+
import { COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, RESOURCES } from '../../constants';
67
import { PERMISSION } from '../../permissions/constants';
78

9+
810
module.exports = [
911
(req, res, next) => {
1012
if (!util.hasPermissionByReq(PERMISSION.CANCEL_COPILOT_OPPORTUNITY, req)) {
@@ -54,6 +56,14 @@ module.exports = [
5456
}));
5557
});
5658

59+
const allInvites = await models.ProjectMemberInvite.findAll({
60+
where: {
61+
applicationId: {
62+
[Op.in]: applications.map(item => item.id),
63+
},
64+
},
65+
});
66+
5767
await Promise.all(promises);
5868

5969
await copilotRequest.update({
@@ -68,6 +78,21 @@ module.exports = [
6878
transaction,
6979
});
7080

81+
// update all the existing invites which are
82+
// associated to the copilot opportunity
83+
// with cancel status
84+
for (const invite of allInvites) {
85+
await invite.update({
86+
status: INVITE_STATUS.CANCELED,
87+
});
88+
await invite.reload();
89+
util.sendResourceToKafkaBus(
90+
req,
91+
EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_UPDATED,
92+
RESOURCES.PROJECT_MEMBER_INVITE,
93+
invite.toJSON());
94+
}
95+
7196
res.status(200).send({ id: opportunity.id });
7297
})
7398

src/routes/copilotOpportunity/list.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,25 @@ module.exports = [
2121
const pageSize = parseInt(req.query.pageSize, 10) || DEFAULT_PAGE_SIZE;
2222
const offset = (page - 1) * pageSize;
2323
const limit = pageSize;
24+
const noGroupingByStatus = req.query.noGrouping === 'true';
25+
26+
const baseOrder = [];
27+
28+
// If grouping is enabled (default), add custom ordering based on status
29+
if (!noGroupingByStatus) {
30+
baseOrder.push([
31+
models.Sequelize.literal(`
32+
CASE
33+
WHEN "CopilotOpportunity"."status" = 'active' THEN 0
34+
WHEN "CopilotOpportunity"."status" = 'cancelled' THEN 1
35+
WHEN "CopilotOpportunity"."status" = 'completed' THEN 2
36+
ELSE 3
37+
END
38+
`),
39+
'ASC',
40+
]);
41+
}
42+
baseOrder.push([sortParams[0], sortParams[1]]);
2443

2544
return models.CopilotOpportunity.findAll({
2645
include: [
@@ -34,7 +53,7 @@ module.exports = [
3453
attributes: ['name'],
3554
},
3655
],
37-
order: [[sortParams[0], sortParams[1]]],
56+
order: baseOrder,
3857
limit,
3958
offset,
4059
})

src/routes/copilotOpportunityApply/create.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import util from '../../util';
88
import { PERMISSION } from '../../permissions/constants';
99
import { CONNECT_NOTIFICATION_EVENT, COPILOT_OPPORTUNITY_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants';
1010
import { createEvent } from '../../services/busApi';
11+
import { getCopilotTypeLabel } from '../../utils/copilot';
1112

1213
const applyCopilotRequestValidations = {
1314
body: Joi.object().keys({
14-
notes: Joi.string().optional(),
15+
notes: Joi.string().required(),
1516
}),
1617
};
1718

@@ -41,6 +42,12 @@ module.exports = [
4142
where: {
4243
id: copilotOpportunityId,
4344
},
45+
include: [
46+
{
47+
model: models.CopilotRequest,
48+
as: 'copilotRequest',
49+
},
50+
],
4451
}).then(async (opportunity) => {
4552
if (!opportunity) {
4653
const err = new Error('No opportunity found');
@@ -92,12 +99,15 @@ module.exports = [
9299

93100
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
94101
const copilotPortalUrl = config.get('copilotPortalUrl');
102+
const requestData = opportunity.copilotRequest.data;
95103
listOfSubjects.forEach((subject) => {
96104
createEvent(emailEventType, {
97105
data: {
98106
user_name: subject.handle,
99107
opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}#applications`,
100108
work_manager_url: config.get('workManagerUrl'),
109+
opportunity_type: getCopilotTypeLabel(requestData.projectType),
110+
opportunity_title: requestData.opportunityTitle,
101111
},
102112
sendgrid_template_id: TEMPLATE_IDS.APPLY_COPILOT,
103113
recipients: [subject.email],

src/routes/copilotOpportunityApply/list.js

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,68 @@ module.exports = [
3131
canAccessAllApplications ? {} : { createdBy: userId },
3232
);
3333

34-
return models.CopilotApplication.findAll({
35-
where: whereCondition,
36-
include: [
37-
{
38-
model: models.CopilotOpportunity,
39-
as: 'copilotOpportunity',
40-
},
41-
],
42-
order: [[sortParams[0], sortParams[1]]],
34+
return models.CopilotOpportunity.findOne({
35+
where: {
36+
id: opportunityId,
37+
}
38+
}).then((opportunity) => {
39+
if (!opportunity) {
40+
const err = new Error('No opportunity found');
41+
err.status = 404;
42+
throw err;
43+
}
44+
return models.CopilotApplication.findAll({
45+
where: whereCondition,
46+
include: [
47+
{
48+
model: models.CopilotOpportunity,
49+
as: 'copilotOpportunity',
50+
},
51+
],
52+
order: [[sortParams[0], sortParams[1]]],
53+
})
54+
.then(copilotApplications => {
55+
req.log.debug(`CopilotApplications ${JSON.stringify(copilotApplications)}`);
56+
return models.ProjectMember.getActiveProjectMembers(opportunity.projectId).then((members) => {
57+
req.log.debug(`Fetched existing active members ${JSON.stringify(members)}`);
58+
req.log.debug(`Applications ${JSON.stringify(copilotApplications)}`);
59+
const enrichedApplications = copilotApplications.map(application => {
60+
const m = members.find(m => m.userId === application.userId);
61+
62+
// Using spread operator fails in lint check
63+
// While Object.assign fails silently during run time
64+
// So using this method
65+
const enriched = {
66+
id: application.id,
67+
opportunityId: application.opportunityId,
68+
notes: application.notes,
69+
status: application.status,
70+
userId: application.userId,
71+
deletedAt: application.deletedAt,
72+
createdAt: application.createdAt,
73+
updatedAt: application.updatedAt,
74+
deletedBy: application.deletedBy,
75+
createdBy: application.createdBy,
76+
updatedBy: application.updatedBy,
77+
copilotOpportunity: application.copilotOpportunity,
78+
};
79+
80+
if (m) {
81+
enriched.existingMembership = m;
82+
}
83+
84+
req.log.debug(`Existing member to application ${JSON.stringify(enriched)}`);
85+
86+
return enriched;
87+
});
88+
89+
req.log.debug(`Enriched Applications ${JSON.stringify(enrichedApplications)}`);
90+
res.status(200).send(enrichedApplications);
91+
});
92+
})
4393
})
44-
.then(copilotApplications => res.json(copilotApplications))
45-
.catch((err) => {
46-
util.handleError('Error fetching copilot applications', err, req, next);
47-
});
94+
.catch((err) => {
95+
util.handleError('Error fetching copilot applications', err, req, next);
96+
});
4897
},
4998
];

0 commit comments

Comments
 (0)