Skip to content

Commit 6224d47

Browse files
authored
Merge pull request #809 from topcoder-platform/pm-1168_1
fix(PM-1168): invite user when assigning as copilot
2 parents 944692c + 3a4fc53 commit 6224d47

File tree

7 files changed

+167
-22
lines changed

7 files changed

+167
-22
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-1168']
152+
only: ['develop', 'migration-setup']
153153
- deployProd:
154154
context : org-global
155155
filters:
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module.exports = {
2+
up: async (queryInterface, Sequelize) => {
3+
await queryInterface.addColumn('project_member_invites', 'applicationId', {
4+
type: Sequelize.BIGINT,
5+
allowNull: true,
6+
references: {
7+
model: 'copilot_applications',
8+
key: 'id',
9+
},
10+
onUpdate: 'CASCADE',
11+
onDelete: 'SET NULL',
12+
});
13+
},
14+
15+
down: async (queryInterface) => {
16+
await queryInterface.removeColumn('project_member_invites', 'applicationId');
17+
},
18+
};

src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const COPILOT_REQUEST_STATUS = {
2020

2121
export const COPILOT_APPLICATION_STATUS = {
2222
PENDING: 'pending',
23+
INVITED: 'invited',
2324
ACCEPTED: 'accepted',
2425
};
2526

src/models/projectMember.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,12 @@ module.exports = function defineProjectMember(sequelize, DataTypes) {
4444
})
4545
.then(res => _.without(_.map(res, 'projectId'), null));
4646

47-
ProjectMember.getActiveProjectMembers = projectId => ProjectMember.findAll({
47+
ProjectMember.getActiveProjectMembers = (projectId, t) => ProjectMember.findAll({
4848
where: {
4949
deletedAt: { $eq: null },
5050
projectId,
5151
},
52+
transaction: t,
5253
raw: true,
5354
});
5455

src/models/projectMemberInvite.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ module.exports = function defineProjectMemberInvite(sequelize, DataTypes) {
1313
isEmail: true,
1414
},
1515
},
16+
applicationId: {
17+
type: DataTypes.BIGINT,
18+
allowNull: true,
19+
references: {
20+
model: 'copilot_applications',
21+
key: 'id',
22+
},
23+
onUpdate: 'CASCADE',
24+
onDelete: 'CASCADE',
25+
},
1626
role: {
1727
type: DataTypes.STRING,
1828
allowNull: false,

src/routes/copilotOpportunity/assign.js

Lines changed: 85 additions & 14 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_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS } from '../../constants';
9+
import { createEvent } from '../../services/busApi';
10+
import { CONNECT_NOTIFICATION_EVENT, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, PROJECT_MEMBER_ROLE, RESOURCES } from '../../constants';
911

1012
const assignCopilotOpportunityValidations = {
1113
body: Joi.object().keys({
@@ -64,7 +66,7 @@ module.exports = [
6466

6567
const projectId = opportunity.projectId;
6668
const userId = application.userId;
67-
const activeMembers = await models.ProjectMember.getActiveProjectMembers(projectId);
69+
const activeMembers = await models.ProjectMember.getActiveProjectMembers(projectId, t);
6870

6971
const existingUser = activeMembers.find(item => item.userId === userId);
7072
if (existingUser && existingUser.role === 'copilot') {
@@ -73,20 +75,89 @@ module.exports = [
7375
throw err;
7476
}
7577

76-
await models.CopilotRequest.update(
77-
{ status: COPILOT_REQUEST_STATUS.FULFILLED },
78-
{ where: { id: opportunity.copilotRequestId }, transaction: t },
79-
);
78+
const project = await models.Project.findOne({
79+
where: {
80+
id: projectId,
81+
},
82+
transaction: t,
83+
});
84+
85+
const existingInvite = await models.ProjectMemberInvite.findAll({
86+
where: {
87+
userId,
88+
projectId,
89+
role: PROJECT_MEMBER_ROLE.COPILOT,
90+
status: INVITE_STATUS.PENDING,
91+
},
92+
transaction: t,
93+
});
94+
95+
if (existingInvite && existingInvite.length) {
96+
const err = new Error(`User already has an pending invite to the project`);
97+
err.status = 400;
98+
throw err;
99+
}
100+
101+
const applicationUser = await util.getMemberDetailsByUserIds([userId], req.log, req.id);
102+
103+
const invite = await models.ProjectMemberInvite.create({
104+
status: INVITE_STATUS.PENDING,
105+
role: PROJECT_MEMBER_ROLE.COPILOT,
106+
userId,
107+
projectId,
108+
applicationId: application.id,
109+
email: applicationUser[0].email,
110+
createdBy: req.authUser.userId,
111+
createdAt: new Date(),
112+
updatedBy: req.authUser.userId,
113+
updatedAt: new Date(),
114+
}, {
115+
transaction: t,
116+
})
80117

81-
await opportunity.update(
82-
{ status: COPILOT_OPPORTUNITY_STATUS.COMPLETED },
83-
{ transaction: t },
84-
);
118+
util.sendResourceToKafkaBus(
119+
req,
120+
EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED,
121+
RESOURCES.PROJECT_MEMBER_INVITE,
122+
invite.toJSON());
85123

86-
await models.CopilotApplication.update(
87-
{ status: COPILOT_APPLICATION_STATUS.ACCEPTED },
88-
{ where: { id: applicationId }, transaction: t },
89-
);
124+
const authUserDetails = await util.getMemberDetailsByUserIds([req.authUser.userId], req.log, req.id);
125+
126+
const emailEventType = CONNECT_NOTIFICATION_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED;
127+
await createEvent(emailEventType, {
128+
data: {
129+
workManagerUrl: config.get('workManagerUrl'),
130+
accountsAppURL: config.get('accountsAppUrl'),
131+
subject: config.get('inviteEmailSubject'),
132+
projects: [{
133+
name: project.name,
134+
projectId,
135+
sections: [
136+
{
137+
EMAIL_INVITES: true,
138+
title: config.get('inviteEmailSectionTitle'),
139+
projectName: project.name,
140+
projectId,
141+
initiator: authUserDetails[0],
142+
isSSO: util.isSSO(project),
143+
},
144+
],
145+
}],
146+
},
147+
recipients: [applicationUser[0].email],
148+
version: 'v3',
149+
from: {
150+
name: config.get('EMAIL_INVITE_FROM_NAME'),
151+
email: config.get('EMAIL_INVITE_FROM_EMAIL'),
152+
},
153+
categories: [`${process.env.NODE_ENV}:${emailEventType}`.toLowerCase()],
154+
}, req.log);
155+
156+
await application.update({
157+
status: COPILOT_APPLICATION_STATUS.INVITED,
158+
}, {
159+
transaction: t,
160+
});
90161

91162
res.status(200).send({ id: applicationId });
92163
}).catch(err => next(err));

src/routes/projectMemberInvites/update.js

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Joi from 'joi';
44
import { middleware as tcMiddleware } from 'tc-core-library-js';
55
import models from '../../models';
66
import util from '../../util';
7-
import { INVITE_STATUS, EVENT, RESOURCES } from '../../constants';
7+
import { INVITE_STATUS, EVENT, RESOURCES, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS } from '../../constants';
88
import { PERMISSION } from '../../permissions/constants';
99

1010
/**
@@ -94,7 +94,7 @@ module.exports = [
9494
if (updatedInvite.status === INVITE_STATUS.ACCEPTED ||
9595
updatedInvite.status === INVITE_STATUS.REQUEST_APPROVED) {
9696
return models.ProjectMember.getActiveProjectMembers(projectId)
97-
.then((members) => {
97+
.then(async (members) => {
9898
req.context = req.context || {};
9999
req.context.currentProjectMembers = members;
100100
let userId = updatedInvite.userId;
@@ -117,10 +117,54 @@ module.exports = [
117117
createdBy: req.authUser.userId,
118118
updatedBy: req.authUser.userId,
119119
};
120-
return util
121-
.addUserToProject(req, member)
122-
.then(() => res.json(util.postProcessInvites('$.email', updatedInvite, req)))
123-
.catch(err => next(err));
120+
const t = await models.sequelize.transaction();
121+
try {
122+
await util.addUserToProject(req, member, t);
123+
if (invite.applicationId) {
124+
const application = await models.CopilotApplication.findOne({
125+
where: {
126+
id: invite.applicationId,
127+
},
128+
transaction: t,
129+
});
130+
131+
await application.update({ status: COPILOT_APPLICATION_STATUS.ACCEPTED }, {
132+
transaction: t
133+
});
134+
135+
const opportunity = await models.CopilotOpportunity.findOne({
136+
where: {
137+
id: application.opportunityId,
138+
},
139+
transaction: t,
140+
});
141+
142+
await opportunity.update({
143+
status: COPILOT_OPPORTUNITY_STATUS.COMPLETED
144+
}, {
145+
transaction: t,
146+
});
147+
148+
const request = await models.CopilotRequest.findOne({
149+
where: {
150+
id: opportunity.copilotRequestId,
151+
},
152+
transaction: t,
153+
});
154+
155+
await request.update({
156+
status: COPILOT_REQUEST_STATUS.FULFILLED
157+
}, {
158+
transaction: t,
159+
});
160+
}
161+
162+
await t.commit();
163+
return res.json(util.postProcessInvites('$.email', updatedInvite, req));
164+
} catch (e) {
165+
await t.rollback();
166+
return next(e);
167+
}
124168
});
125169
}
126170
return res.json(util.postProcessInvites('$.email', updatedInvite, req));

0 commit comments

Comments
 (0)