Skip to content

Commit 8f091ff

Browse files
Merge branch 'develop' into deploy-pm-1612
2 parents eff16df + 39962a7 commit 8f091ff

File tree

9 files changed

+257
-62
lines changed

9 files changed

+257
-62
lines changed

src/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,9 @@ export const TEMPLATE_IDS = {
313313
PROJECT_MEMBER_INVITED: 'd-b47a25b103604bc28fc0ce77e77fb681',
314314
INFORM_PM_COPILOT_APPLICATION_ACCEPTED: 'd-b35d073e302b4279a1bd208fcfe96f58',
315315
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'
316319
}
317320
export const REGEX = {
318321
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: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import config from 'config';
66
import models from '../../models';
77
import util from '../../util';
88
import { PERMISSION } from '../../permissions/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';
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';
1010
import { getCopilotTypeLabel } from '../../utils/copilot';
1111
import { createEvent } from '../../services/busApi';
1212
import moment from 'moment';
13+
import { Op } from 'sequelize';
1314

1415
const assignCopilotOpportunityValidations = {
1516
body: Joi.object().keys({
@@ -31,6 +32,34 @@ module.exports = [
3132
return next(err);
3233
}
3334

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+
3463
return models.sequelize.transaction(async (t) => {
3564
const opportunity = await models.CopilotOpportunity.findOne({
3665
where: { id: copilotOpportunityId },
@@ -172,51 +201,82 @@ module.exports = [
172201
return;
173202
}
174203

175-
const existingInvite = await models.ProjectMemberInvite.findAll({
176-
where: {
177-
userId,
178-
projectId,
179-
role: PROJECT_MEMBER_ROLE.COPILOT,
180-
status: INVITE_STATUS.PENDING,
181-
},
182-
transaction: t,
183-
});
184-
185-
if (existingInvite && existingInvite.length) {
186-
const err = new Error(`User already has an pending invite to the project`);
187-
err.status = 400;
188-
throw err;
189-
}
190-
191-
const invite = await models.ProjectMemberInvite.create({
192-
status: INVITE_STATUS.PENDING,
193-
role: PROJECT_MEMBER_ROLE.COPILOT,
194-
userId,
204+
const member = {
195205
projectId,
196-
applicationId: application.id,
206+
role: USER_ROLE.TC_COPILOT,
207+
userId,
197208
createdBy: req.authUser.userId,
198-
createdAt: new Date(),
199209
updatedBy: req.authUser.userId,
200-
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,
217+
}, {
218+
transaction: t,
219+
});
220+
221+
await opportunity.update({
222+
status: COPILOT_OPPORTUNITY_STATUS.COMPLETED,
201223
}, {
202224
transaction: t,
203-
})
225+
});
204226

205-
util.sendResourceToKafkaBus(
206-
req,
207-
EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED,
208-
RESOURCES.PROJECT_MEMBER_INVITE,
209-
Object.assign({}, invite.toJSON(), {
210-
source: 'copilot_portal',
211-
}),
212-
);
213227

214-
await application.update({
215-
status: COPILOT_APPLICATION_STATUS.INVITED,
228+
await copilotRequest.update({
229+
status: COPILOT_REQUEST_STATUS.FULFILLED,
216230
}, {
217231
transaction: t,
218232
});
219233

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+
220280
res.status(200).send({ id: applicationId });
221281
}).catch(err => next(err));
222282
},

src/routes/copilotOpportunity/delete.js

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import _ from 'lodash';
2+
import { Op } from 'sequelize';
3+
import config from 'config';
24

35
import models from '../../models';
46
import util from '../../util';
5-
import { COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS } from '../../constants';
7+
import { CONNECT_NOTIFICATION_EVENT, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, RESOURCES, TEMPLATE_IDS } from '../../constants';
8+
import { createEvent } from '../../services/busApi';
69
import { PERMISSION } from '../../permissions/constants';
710

11+
812
module.exports = [
913
(req, res, next) => {
1014
if (!util.hasPermissionByReq(PERMISSION.CANCEL_COPILOT_OPPORTUNITY, req)) {
@@ -18,6 +22,32 @@ module.exports = [
1822
// default values
1923
const opportunityId = _.parseInt(req.params.id);
2024

25+
const sendEmailToAllApplicants = async (copilotRequest, applications) => {
26+
const userIds = applications.map(item => item.userId);
27+
const users = await util.getMemberDetailsByUserIds(userIds, req.log, req.id);
28+
29+
users.forEach(async (user) => {
30+
req.log.debug(`Sending email notification to copilots who applied`);
31+
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
32+
const copilotPortalUrl = config.get('copilotPortalUrl');
33+
const requestData = copilotRequest.data;
34+
createEvent(emailEventType, {
35+
data: {
36+
opportunity_details_url: copilotPortalUrl,
37+
work_manager_url: config.get('workManagerUrl'),
38+
opportunity_title: requestData.opportunityTitle,
39+
user_name: user ? user.handle : "",
40+
},
41+
sendgrid_template_id: TEMPLATE_IDS.COPILOT_OPPORTUNITY_CANCELED,
42+
recipients: [user.email],
43+
version: 'v3',
44+
}, req.log);
45+
46+
req.log.debug(`Email sent to copilots who applied`);
47+
});
48+
49+
};
50+
2151
return models.sequelize.transaction(async (transaction) => {
2252
req.log.debug('Canceling Copilot opportunity transaction', opportunityId);
2353
const opportunity = await models.CopilotOpportunity.findOne({
@@ -54,6 +84,14 @@ module.exports = [
5484
}));
5585
});
5686

87+
const allInvites = await models.ProjectMemberInvite.findAll({
88+
where: {
89+
applicationId: {
90+
[Op.in]: applications.map(item => item.id),
91+
},
92+
},
93+
});
94+
5795
await Promise.all(promises);
5896

5997
await copilotRequest.update({
@@ -68,6 +106,23 @@ module.exports = [
68106
transaction,
69107
});
70108

109+
// update all the existing invites which are
110+
// associated to the copilot opportunity
111+
// with cancel status
112+
for (const invite of allInvites) {
113+
await invite.update({
114+
status: INVITE_STATUS.CANCELED,
115+
});
116+
await invite.reload();
117+
util.sendResourceToKafkaBus(
118+
req,
119+
EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_UPDATED,
120+
RESOURCES.PROJECT_MEMBER_INVITE,
121+
invite.toJSON());
122+
}
123+
124+
await sendEmailToAllApplicants(copilotRequest, applications)
125+
71126
res.status(200).send({ id: opportunity.id });
72127
})
73128

src/routes/copilotOpportunity/get.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import util from '../../util';
44
module.exports = [
55
(req, res, next) => {
66
const { id } = req.params;
7-
87
if (!id || isNaN(id)) {
98
return util.handleError('Invalid opportunity ID', null, req, next, 400);
109
}

src/routes/copilotOpportunity/list.js

Lines changed: 20 additions & 4 deletions
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,10 +53,7 @@ module.exports = [
3453
attributes: ['name'],
3554
},
3655
],
37-
order: [
38-
[models.Sequelize.literal(`CASE WHEN "CopilotOpportunity"."status" = 'active' THEN 0 ELSE 1 END`), 'ASC'],
39-
[sortParams[0], sortParams[1]]
40-
],
56+
order: baseOrder,
4157
limit,
4258
offset,
4359
})

src/routes/copilotRequest/approveRequest.service.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import moment from 'moment';
44
import { Op } from 'sequelize';
55

66
import models from '../../models';
7-
import { CONNECT_NOTIFICATION_EVENT, COPILOT_REQUEST_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants';
7+
import { CONNECT_NOTIFICATION_EVENT, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants';
88
import util from '../../util';
99
import { createEvent } from '../../services/busApi';
1010
import { getCopilotTypeLabel } from '../../utils/copilot';
@@ -46,13 +46,13 @@ module.exports = (req, data, existingTransaction) => {
4646
projectId,
4747
type: data.type,
4848
status: {
49-
[Op.notIn]: [COPILOT_REQUEST_STATUS.CANCELED],
49+
[Op.in]: [COPILOT_OPPORTUNITY_STATUS.ACTIVE],
5050
}
5151
},
5252
})
5353
.then((existingCopilotOpportunityOfSameType) => {
5454
if (existingCopilotOpportunityOfSameType) {
55-
const err = new Error('There\'s an opportunity of same type already!');
55+
const err = new Error('There\'s an active opportunity of same type already!');
5656
_.assign(err, {
5757
status: 403,
5858
});

0 commit comments

Comments
 (0)