diff --git a/.circleci/config.yml b/.circleci/config.yml index e2ac728e0..96e8ba80b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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: diff --git a/config/development.json b/config/development.json index 878bc6693..5b8eb1360 100644 --- a/config/development.json +++ b/config/development.json @@ -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", diff --git a/src/routes/copilotRequest/approveRequest.service.js b/src/routes/copilotRequest/approveRequest.service.js index 5ef4d2c12..ed34c9df5 100644 --- a/src/routes/copilotRequest/approveRequest.service.js +++ b/src/routes/copilotRequest/approveRequest.service.js @@ -1,10 +1,15 @@ -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'; @@ -17,85 +22,86 @@ 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 { + 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 }); + + 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 }); + + 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); + + 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 + } + }); }; diff --git a/src/routes/copilotRequest/create.js b/src/routes/copilotRequest/create.js index 52b640ea6..0fcf1aaf7 100644 --- a/src/routes/copilotRequest/create.js +++ b/src/routes/copilotRequest/create.js @@ -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'); @@ -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('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; + } }, ]; + diff --git a/src/util.js b/src/util.js index 60c71d57e..98e097c7f 100644 --- a/src/util.js +++ b/src/util.js @@ -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', @@ -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); } }), @@ -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; + logger.debug(`Roles by ${roleName}: ${JSON.stringify(roles)}`); + return roles .filter(item => item.roleName === roleName) .map(r => r.id); });