Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2474181
fix: paginated copilot request
hentrymartin Aug 15, 2025
e65a766
fix: paginated copilot request
hentrymartin Aug 15, 2025
e439cc8
fix: paginated copilot request
hentrymartin Aug 15, 2025
e30591f
fix: paginated copilot request
hentrymartin Aug 15, 2025
d4a88f4
fix: paginated copilot request
hentrymartin Aug 15, 2025
902e2b3
fix: add project in copilot request
hentrymartin Aug 17, 2025
44047b6
fix: added project relation to copilot requests
hentrymartin Aug 17, 2025
e7888e9
feat: send email when opportunity is completed or canceled
hentrymartin Aug 18, 2025
4aa668c
feat: send email when opportunity is completed or canceled
hentrymartin Aug 18, 2025
83c6db9
feat: send email when opportunity is completed or canceled
hentrymartin Aug 18, 2025
5071cb8
fix: build
hentrymartin Aug 18, 2025
c55231a
debug
hentrymartin Aug 18, 2025
ea6cd97
debug
hentrymartin Aug 18, 2025
4e0bf89
fix: assign email
hentrymartin Aug 18, 2025
39acdfa
fix: assign email
hentrymartin Aug 18, 2025
76ec16c
removed debug messages
hentrymartin Aug 19, 2025
d2425b4
fix: typo
hentrymartin Aug 19, 2025
c958c3c
Merge pull request #849 from topcoder-platform/pm-1611
hentrymartin Aug 19, 2025
b8343b9
fix: added sorting functionality
hentrymartin Aug 19, 2025
dea96ef
fix: added sorting functionality
hentrymartin Aug 19, 2025
8989bbe
fix: added sorting functionality
hentrymartin Aug 19, 2025
07e16d1
fix: sorting
hentrymartin Aug 19, 2025
b793329
Merge pull request #850 from topcoder-platform/pm-1650_1
kkartunov Aug 20, 2025
317c097
fix: url in cancel and completed email template
hentrymartin Aug 20, 2025
fee1141
fix: url in cancel and completed email template
hentrymartin Aug 20, 2025
f7d96b3
fix: url in cancel and completed email template
hentrymartin Aug 20, 2025
39962a7
Merge pull request #851 from topcoder-platform/pm-1611_1
hentrymartin Aug 20, 2025
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-1613']
only: ['develop', 'migration-setup', 'pm-1611_1']
- deployProd:
context : org-global
filters:
Expand Down
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ export const TEMPLATE_IDS = {
INFORM_PM_COPILOT_APPLICATION_ACCEPTED: 'd-b35d073e302b4279a1bd208fcfe96f58',
COPILOT_ALREADY_PART_OF_PROJECT: 'd-003d41cdc9de4bbc9e14538e8f2e0585',
COPILOT_APPLICATION_ACCEPTED: 'd-eef5e7568c644940b250e76d026ced5b',
COPILOT_OPPORTUNITY_COMPLETED: 'd-dc448919d11b4e7d8b4ba351c4b67b8b',
COPILOT_OPPORTUNITY_CANCELED: 'd-2a67ba71e82f4d70891fe6989c3522a3'
}
export const REGEX = {
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
Expand Down
9 changes: 5 additions & 4 deletions src/models/copilotRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import _ from 'lodash';
import { COPILOT_REQUEST_STATUS } from '../constants';

module.exports = function defineCopilotRequest(sequelize, DataTypes) {
const CopliotRequest = sequelize.define('CopilotRequest', {
const CopilotRequest = sequelize.define('CopilotRequest', {
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
status: {
type: DataTypes.STRING(16),
Expand Down Expand Up @@ -30,9 +30,10 @@ module.exports = function defineCopilotRequest(sequelize, DataTypes) {
indexes: [],
});

CopliotRequest.associate = (models) => {
CopliotRequest.hasMany(models.CopilotOpportunity, { as: 'copilotOpportunity', foreignKey: 'copilotRequestId' });
CopilotRequest.associate = (models) => {

Choose a reason for hiding this comment

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

Typo in the model name: CopliotRequest should be corrected to CopilotRequest.

CopilotRequest.hasMany(models.CopilotOpportunity, { as: 'copilotOpportunity', foreignKey: 'copilotRequestId' });
CopilotRequest.belongsTo(models.Project, { as: 'project', foreignKey: 'projectId' });
};

Choose a reason for hiding this comment

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

Typo in the return statement: CopliotRequest should be corrected to CopilotRequest.

return CopliotRequest;
return CopilotRequest;
};
31 changes: 31 additions & 0 deletions src/routes/copilotOpportunity/assign.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,34 @@ module.exports = [
return next(err);
}

const sendEmailToAllApplicants = async (copilotRequest, allApplications) => {

const userIds = allApplications.map(item => item.userId);

const users = await util.getMemberDetailsByUserIds(userIds, req.log, req.id);

users.forEach(async (user) => {

Choose a reason for hiding this comment

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

Using forEach with an async function does not wait for the asynchronous operations to complete. Consider using a for...of loop or Promise.all to ensure all emails are sent before proceeding.

req.log.debug(`Sending email notification to copilots who are not accepted`);
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
const copilotPortalUrl = config.get('copilotPortalUrl');
const requestData = copilotRequest.data;
createEvent(emailEventType, {
data: {
opportunity_details_url: copilotPortalUrl,

Choose a reason for hiding this comment

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

The opportunity_details_url is set to copilotPortalUrl, which might not be specific to the opportunity. Ensure this URL is correct and points to the intended resource.

work_manager_url: config.get('workManagerUrl'),
opportunity_title: requestData.opportunityTitle,
user_name: user ? user.handle : "",

Choose a reason for hiding this comment

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

Consider handling the case where user might be null or undefined more explicitly to avoid potential errors.

},
sendgrid_template_id: TEMPLATE_IDS.COPILOT_OPPORTUNITY_COMPLETED,
recipients: [user.email],
version: 'v3',
}, req.log);

req.log.debug(`Email sent to copilots who are not accepted`);
});

};

return models.sequelize.transaction(async (t) => {
const opportunity = await models.CopilotOpportunity.findOne({
where: { id: copilotOpportunityId },
Expand Down Expand Up @@ -238,6 +266,9 @@ module.exports = [
transaction: t,
});

// Send email to all applicants about opportunity completion
await sendEmailToAllApplicants(copilotRequest, otherApplications);

Choose a reason for hiding this comment

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

Consider handling potential errors from sendEmailToAllApplicants using a try-catch block to ensure that any issues with sending emails do not affect the subsequent operations.


for (const otherApplication of otherApplications) {
await otherApplication.update({
status: COPILOT_APPLICATION_STATUS.CANCELED,
Expand Down
32 changes: 31 additions & 1 deletion src/routes/copilotOpportunity/delete.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import _ from 'lodash';
import { Op } from 'sequelize';
import config from 'config';

Choose a reason for hiding this comment

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

Consider checking if the config module is actually used in this file. If not, it might be unnecessary to import it.


import models from '../../models';
import util from '../../util';
import { COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, RESOURCES } from '../../constants';
import { CONNECT_NOTIFICATION_EVENT, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, RESOURCES, TEMPLATE_IDS } from '../../constants';
import { createEvent } from '../../services/busApi';

Choose a reason for hiding this comment

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

Ensure that the createEvent function from busApi is used in this file. If not, the import might be redundant.

import { PERMISSION } from '../../permissions/constants';


Expand All @@ -20,6 +22,32 @@ module.exports = [
// default values
const opportunityId = _.parseInt(req.params.id);

const sendEmailToAllApplicants = async (copilotRequest, applications) => {
const userIds = applications.map(item => item.userId);
const users = await util.getMemberDetailsByUserIds(userIds, req.log, req.id);

users.forEach(async (user) => {

Choose a reason for hiding this comment

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

Consider using Promise.all to handle the asynchronous operations in the forEach loop. The current implementation may not wait for all emails to be sent before proceeding, which could lead to unexpected behavior.

req.log.debug(`Sending email notification to copilots who applied`);
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
const copilotPortalUrl = config.get('copilotPortalUrl');
const requestData = copilotRequest.data;
createEvent(emailEventType, {
data: {
opportunity_details_url: copilotPortalUrl,
work_manager_url: config.get('workManagerUrl'),
opportunity_title: requestData.opportunityTitle,
user_name: user ? user.handle : "",

Choose a reason for hiding this comment

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

The ternary operation user ? user.handle : "" could be simplified by using optional chaining and nullish coalescing: user?.handle ?? "".

},
sendgrid_template_id: TEMPLATE_IDS.COPILOT_OPPORTUNITY_CANCELED,
recipients: [user.email],
version: 'v3',
}, req.log);

req.log.debug(`Email sent to copilots who applied`);
});

};

return models.sequelize.transaction(async (transaction) => {
req.log.debug('Canceling Copilot opportunity transaction', opportunityId);
const opportunity = await models.CopilotOpportunity.findOne({
Expand Down Expand Up @@ -93,6 +121,8 @@ module.exports = [
invite.toJSON());
}

await sendEmailToAllApplicants(copilotRequest, applications)

Choose a reason for hiding this comment

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

It seems like the function sendEmailToAllApplicants is being called without checking if applications is not empty or null. Consider adding a condition to ensure applications contains valid data before proceeding with the email sending operation to avoid potential errors.


res.status(200).send({ id: opportunity.id });
})

Expand Down
1 change: 0 additions & 1 deletion src/routes/copilotOpportunity/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import util from '../../util';
module.exports = [
(req, res, next) => {
const { id } = req.params;

if (!id || isNaN(id)) {
return util.handleError('Invalid opportunity ID', null, req, next, 400);
}
Expand Down
62 changes: 49 additions & 13 deletions src/routes/copilotRequest/list.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import _ from 'lodash';
import { Op, Sequelize } from 'sequelize';

import models from '../../models';
import util from '../../util';
import { PERMISSION } from '../../permissions/constants';
import { DEFAULT_PAGE_SIZE } from '../../constants';

module.exports = [
(req, res, next) => {
Expand All @@ -15,33 +17,67 @@ module.exports = [
return next(err);
}

const page = parseInt(req.query.page, 10) || 1;
const pageSize = parseInt(req.query.pageSize, 10) || DEFAULT_PAGE_SIZE;

Choose a reason for hiding this comment

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

Consider defining DEFAULT_PAGE_SIZE explicitly in this file or importing it from a constants file to ensure clarity and maintainability.

const offset = (page - 1) * pageSize;

const projectId = _.parseInt(req.params.projectId);

let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'createdAt desc';
if (sort.indexOf(' ') === -1) {
sort += ' asc';
}
const sortableProps = ['createdAt asc', 'createdAt desc'];
const sortableProps = [
'createdAt asc',
'createdAt desc',
'projectName asc',
'projectName desc',
'opportunityTitle asc',
'opportunityTitle desc',
'projectType asc',
'projectType desc',
'status asc',
'status desc',
];
if (_.indexOf(sortableProps, sort) < 0) {
return util.handleError('Invalid sort criteria', null, req, next);
}
const sortParams = sort.split(' ');
let sortParams = sort.split(' ');
let order = [[sortParams[0], sortParams[1]]];
const relationBasedSortParams = ['projectName'];

Choose a reason for hiding this comment

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

The variable relationBasedSortParams is defined but only used for checking inclusion. Consider adding a comment or documentation to clarify its purpose and usage.

const jsonBasedSortParams = ['opportunityTitle', 'projectType'];

Choose a reason for hiding this comment

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

The variable jsonBasedSortParams is defined but only used for checking inclusion. Consider adding a comment or documentation to clarify its purpose and usage.

if (relationBasedSortParams.includes(sortParams[0])) {
order = [
[{model: models.Project, as: 'project'}, 'name', sortParams[1]],
['id', 'DESC']
]
}

if (jsonBasedSortParams.includes(sortParams[0])) {
order = [
[models.sequelize.literal(`("CopilotRequest"."data"->>'${sortParams[0]}')`), sortParams[1]],

Choose a reason for hiding this comment

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

Using sequelize.literal for JSON-based sorting can be error-prone if the JSON structure changes. Ensure that the JSON keys are consistently structured or add validation to handle potential errors.

['id', 'DESC'],
]
}

const whereCondition = projectId ? { projectId } : {};

return models.CopilotRequest.findAll({
return models.CopilotRequest.findAndCountAll({
where: whereCondition,
include: [
{
model: models.CopilotOpportunity,
as: 'copilotOpportunity',
},
{ model: models.CopilotOpportunity, as: 'copilotOpportunity', required: false },
{ model: models.Project, as: 'project', required: false },
],
order: [[sortParams[0], sortParams[1]]],
})
.then(copilotRequests => res.json(copilotRequests))
.catch((err) => {
util.handleError('Error fetching copilot requests', err, req, next);
});
order,
limit: pageSize,
offset,
distinct: true,
subQuery: false,
}).then(({rows: copilotRequests, count}) => util.setPaginationHeaders(req, res, {

Choose a reason for hiding this comment

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

Ensure that util.setPaginationHeaders correctly handles pagination headers and edge cases, such as when count is zero or page exceeds the total number of pages.

count: count,
rows: copilotRequests,
page,
pageSize,
}));
},
];