Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ workflows:
context : org-global
filters:
branches:
only: ['develop', 'migration-setup', 'PM-1612', 'fix-project-exposing']
only: ['develop', 'migration-setup', 'PM-1612', 'fix-2321']
- deployProd:
context : org-global
filters:
Expand Down
2 changes: 1 addition & 1 deletion config/development.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"copilotPortalUrl": "https://copilots.topcoder-dev.com",
"fileServiceEndpoint": "https://api.topcoder-dev.com/v5/files",
"memberServiceEndpoint": "https://api.topcoder-dev.com/v5/members",
"identityServiceEndpoint": "https://api.topcoder-dev.com/v3/",
"identityServiceEndpoint": "https://api.topcoder-dev.com/v6/",
"taasJobApiUrl": "https://api.topcoder-dev.com/v5/jobs",
"sfdcBillingAccountNameField": "Billing_Account_name__c",
"sfdcBillingAccountMarkupField": "Mark_Up__c",
Expand Down
172 changes: 91 additions & 81 deletions src/routes/copilotRequest/approveRequest.service.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import _ from 'lodash';
import config from 'config';
import moment from 'moment';
import { Op } from 'sequelize';

import models from '../../models';
import { CONNECT_NOTIFICATION_EVENT, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants';
import {
CONNECT_NOTIFICATION_EVENT,
COPILOT_OPPORTUNITY_STATUS,
COPILOT_REQUEST_STATUS,
TEMPLATE_IDS, USER_ROLE,
} from '../../constants';
import util from '../../util';
import { createEvent } from '../../services/busApi';
import { getCopilotTypeLabel } from '../../utils/copilot';
Expand All @@ -17,85 +21,91 @@ const resolveTransaction = (transaction, callback) => {
return models.sequelize.transaction(callback);
};

module.exports = (req, data, existingTransaction) => {
module.exports = async (req, data, existingTransaction) => {
const { projectId, copilotRequestId, opportunityTitle, type, startDate } = data;

return resolveTransaction(existingTransaction, transaction =>
models.Project.findOne({
where: { id: projectId, deletedAt: { $eq: null } },
}, { transaction })
.then((existingProject) => {
if (!existingProject) {
const err = new Error(`active project not found for project id ${projectId}`);
err.status = 404;
throw err;
}
return models.CopilotRequest.findByPk(copilotRequestId, { transaction })
.then((existingCopilotRequest) => {
if (!existingCopilotRequest) {
const err = new Error(`no active copilot request found for copilot request id ${copilotRequestId}`);
err.status = 404;
throw err;
}

return existingCopilotRequest.update({
status: COPILOT_REQUEST_STATUS.APPROVED,
}, { transaction }).then(() => models.CopilotOpportunity
.findOne({
where: {
projectId,
type: data.type,
status: {
[Op.in]: [COPILOT_OPPORTUNITY_STATUS.ACTIVE],
}
},
})
.then((existingCopilotOpportunityOfSameType) => {
if (existingCopilotOpportunityOfSameType) {
const err = new Error('There\'s an active opportunity of same type already!');
_.assign(err, {
status: 403,
});
throw err;
}
return models.CopilotOpportunity
.create(data, { transaction });
}))
.then(async (opportunity) => {
const roles = await util.getRolesByRoleName(USER_ROLE.TC_COPILOT, req.log, req.id);
const { subjects = [] } = await util.getRoleInfo(roles[0], req.log, req.id);
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
const copilotPortalUrl = config.get('copilotPortalUrl');
req.log.info("Sending emails to all copilots about new opportunity");

const sendNotification = (userName, recipient) => createEvent(emailEventType, {
data: {
user_name: userName,
opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`,
work_manager_url: config.get('workManagerUrl'),
opportunity_type: getCopilotTypeLabel(type),
opportunity_title: opportunityTitle,
start_date: moment(startDate).format("DD-MM-YYYY"),
},
sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST,
recipients: [recipient],
version: 'v3',
}, req.log);

subjects.forEach(subject => sendNotification(subject.handle, subject.email));

// send email to notify via slack
sendNotification('Copilots', config.copilotsSlackEmail);

req.log.info("Finished sending emails to copilots");

return opportunity;
})
.catch((err) => {
transaction.rollback();
return Promise.reject(err);
});
});
}),
);
return resolveTransaction(existingTransaction, async (transaction) => {
try {
req.log.debug('approveRequest: finding project', { projectId });

const existingProject = await models.Project.findOne({
where: { id: projectId, deletedAt: { $eq: null } },
transaction,
});

if (!existingProject) {
const err = new Error(`active project not found for project id ${projectId}`);
err.status = 404;
throw err;
}

const copilotRequest = await models.CopilotRequest.findByPk(copilotRequestId, { transaction });
req.log.debug('approveRequest: found copilot request', { copilotRequestId: copilotRequest.id });

if (!copilotRequest) {
const err = new Error(`no active copilot request found for copilot request id ${copilotRequestId}`);
err.status = 404;
throw err;
}

await copilotRequest.update({ status: COPILOT_REQUEST_STATUS.APPROVED }, { transaction });
req.log.debug('Copilot request status updated to APPROVED', { copilotRequestId });

const existingOpportunity = await models.CopilotOpportunity.findOne({
where: {
projectId,
type: data.type,
status: { [Op.in]: [COPILOT_OPPORTUNITY_STATUS.ACTIVE] },
},
transaction,
});

if (existingOpportunity) {
const err = new Error('There\'s an active opportunity of same type already!');
err.status = 403;
throw err;
}

const opportunity = await models.CopilotOpportunity.create(data, { transaction });
req.log.debug('Created new copilot opportunity', { opportunityId: opportunity.id });

// Send notifications
try {
const roles = await util.getRolesByRoleName(USER_ROLE.TC_COPILOT, req.log, req.id);
req.log.debug('Roles fetched', { roles });

const { subjects = [] } = await util.getRoleInfo(roles[0], req.log, req.id);
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
const copilotPortalUrl = config.get('copilotPortalUrl');
req.log.info('Sending emails to all copilots about new opportunity');

const sendNotification = (userName, recipient) => createEvent(emailEventType, {
data: {
user_name: userName,
opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`,
work_manager_url: config.get('workManagerUrl'),
opportunity_type: getCopilotTypeLabel(type),
opportunity_title: opportunityTitle,
start_date: moment(startDate).format('DD-MM-YYYY'),
},
sendgrid_template_id: TEMPLATE_IDS.CREATE_REQUEST,
recipients: [recipient],
version: 'v3',
}, req.log);

subjects.forEach(subject => sendNotification(subject.handle, subject.email));

// send email to notify via slack
sendNotification('Copilots', config.copilotsSlackEmail);
req.log.info('Finished sending emails to copilots');
} catch (emailErr) {
req.log.error('Error sending notifications', { error: emailErr });
}

return opportunity;
} catch (err) {
req.log.error('approveRequest failed', { error: err });
throw err; // let outer transaction handle rollback
}
});
};
114 changes: 56 additions & 58 deletions src/routes/copilotRequest/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const addCopilotRequestValidations = {

module.exports = [
validate(addCopilotRequestValidations),
(req, res, next) => {
async (req, res, next) => {
const data = req.body;
if (!util.hasPermissionByReq(PERMISSION.MANAGE_COPILOT_REQUEST, req)) {
const err = new Error('Unable to create copilot request');
Expand All @@ -58,66 +58,64 @@ module.exports = [
updatedBy: req.authUser.userId,
});

return models.sequelize.transaction((transaction) => {
req.log.debug('Create Copilot request transaction', data);
return models.Project.findOne({
where: { id: projectId, deletedAt: { $eq: null } },
})
.then((existingProject) => {
if (!existingProject) {
const err = new Error(`active project not found for project id ${projectId}`);
err.status = 404;
throw err;
}
return models.CopilotRequest.findOne({
where: {
createdBy: req.authUser.userId,
projectId,
status: {
[Op.in]: [COPILOT_REQUEST_STATUS.NEW, COPILOT_REQUEST_STATUS.APPROVED, COPILOT_REQUEST_STATUS.SEEKING],
},
},
}).then((copilotRequest) => {
if (copilotRequest && copilotRequest.data.projectType === data.data.projectType) {
const err = new Error('There\'s a request of same type already!');
_.assign(err, {
status: 400,
});
throw err;
}
try {
const copilotRequest = await models.sequelize.transaction(async (transaction) => {
req.log.debug('Starting create copilot request transaction', { data });

const existingProject = await models.Project.findOne({
where: { id: projectId, deletedAt: { $eq: null } },
transaction,
});

return models.CopilotRequest
.create(data, { transaction });
}).then((copilotRequest) => {
/**
* Automatically approve the copilot request.
*/
const approveData = _.assign({
projectId,
copilotRequestId: copilotRequest.id,
createdBy: req.authUser.userId,
updatedBy: req.authUser.userId,
type: copilotRequest.data.projectType,
opportunityTitle: copilotRequest.data.opportunityTitle,
startDate: copilotRequest.data.startDate,
});
return approveRequest(req, approveData, transaction).then(() => copilotRequest);
}).then(copilotRequest => res.status(201).json(copilotRequest))
.catch((err) => {
try {
transaction.rollback();
} catch (e) {
_.noop(e);
}
return Promise.reject(err);
});
if (!existingProject) {
const err = new Error(`Active project not found for project id ${projectId}`);
err.status = 404;
throw err;
}

const existingRequest = await models.CopilotRequest.findOne({
where: {
createdBy: req.authUser.userId,
projectId,
status: {
[Op.in]: [
COPILOT_REQUEST_STATUS.NEW,
COPILOT_REQUEST_STATUS.APPROVED,
COPILOT_REQUEST_STATUS.SEEKING,
],
},
},
transaction,
});
})
.catch((err) => {
if (err.message) {
_.assign(err, { details: err.message });

if (existingRequest && existingRequest.data.projectType === data.data.projectType) {
const err = new Error('There\'s a request of same type already!');
err.status = 400;
throw err;
}
util.handleError('Error creating copilot request', err, req, next);

const newRequest = await models.CopilotRequest.create(data, { transaction });

await approveRequest(req, {
projectId,
copilotRequestId: newRequest.id,
createdBy: req.authUser.userId,
updatedBy: req.authUser.userId,
type: newRequest.data.projectType,
opportunityTitle: newRequest.data.opportunityTitle,
startDate: newRequest.data.startDate,
}, transaction);

return newRequest;
});

return res.status(201).json(copilotRequest);
} catch (err) {
req.log.error('Error creating copilot request', { error: err });
if (err.message) _.assign(err, { details: err.message });
util.handleError('Error creating copilot request', err, req, next);
return undefined;
}
},
];

11 changes: 6 additions & 5 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -820,10 +820,10 @@ const projectServiceUtils = {
const token = yield this.getM2MToken();
const httpClient = this.getHttpClient({ id: requestId, log: logger });
httpClient.defaults.timeout = 6000;
logger.debug(`${config.identityServiceEndpoint}roles/${roleId}`, "fetching role info");
logger.debug(`${config.identityServiceEndpoint}roles/${roleId}`, 'fetching role info');
return httpClient.get(`${config.identityServiceEndpoint}roles/${roleId}`, {
params: {
fields: `subjects`,
fields: 'subjects',
},
headers: {
'Content-Type': 'application/json',
Expand All @@ -834,7 +834,7 @@ const projectServiceUtils = {
return _.get(res, 'data.result.content', []);
});
} catch (err) {
logger.debug(err, "error on getting role info");
logger.debug(err, 'error on getting role info');
return Promise.reject(err);
}
}),
Expand All @@ -853,8 +853,9 @@ const projectServiceUtils = {
Authorization: `Bearer ${token}`,
},
}).then((res) => {
logger.debug(`Roles by ${roleName}: ${JSON.stringify(res.data.result.content)}`);
return _.get(res, 'data.result.content', [])
const roles = res.data;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using _.get to safely access res.data to avoid potential errors if res.data is undefined. For example: const roles = _.get(res, 'data', []);

logger.debug(`Roles by ${roleName}: ${JSON.stringify(roles)}`);
return roles
.filter(item => item.roleName === roleName)
.map(r => r.id);
});
Expand Down