Skip to content

[PROD] - Copilot Portal fixes and updates #838

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 123 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
123 commits
Select commit Hold shift + click to select a range
99220ae
fix: make notes mandatory
hentrymartin Jul 9, 2025
9b7b337
Merge pull request #827 from topcoder-platform/pm-1468
hentrymartin Jul 10, 2025
8f2fbd5
fix: group by active and then sort by createdAt
hentrymartin Jul 17, 2025
2218b14
fix: group by active and then sort by createdAt
hentrymartin Jul 17, 2025
c7578c5
debug
hentrymartin Jul 18, 2025
9783ab1
debug
hentrymartin Jul 18, 2025
85bc8da
fix: allow creating copilot request if a request is canceled
hentrymartin Jul 18, 2025
b360f9d
Merge pull request #828 from topcoder-platform/pm-1368
kkartunov Jul 21, 2025
4895bce
PM-1498 - support for opportunity title
vas3a Jul 21, 2025
473b060
fix: add extra info copilots email
hentrymartin Jul 21, 2025
b63b2ad
fix: add extra info copilots email
hentrymartin Jul 21, 2025
191cc98
PM-1499 - edit copilot request
vas3a Jul 22, 2025
7ead77e
Merge pull request #829 from topcoder-platform/PM-1498_copilot-opport…
vas3a Jul 22, 2025
1a8d9f2
fix: add extra info copilots email
hentrymartin Jul 22, 2025
e6f3595
fix: add extra info copilots email
hentrymartin Jul 22, 2025
f63c580
fix: add extra info copilots email
hentrymartin Jul 22, 2025
2f46d05
fix: add extra info copilots email
hentrymartin Jul 22, 2025
2201ec4
fix: add extra info copilots email
hentrymartin Jul 22, 2025
36c6e7b
updated from develop
hentrymartin Jul 22, 2025
cf50f64
updated from develop
hentrymartin Jul 22, 2025
56eb2b6
updated from develop
hentrymartin Jul 22, 2025
fed04b8
fix: type
hentrymartin Jul 22, 2025
64091d5
fix: type
hentrymartin Jul 22, 2025
eaf2116
fix: date format
hentrymartin Jul 22, 2025
5bf08be
fix: date format
hentrymartin Jul 22, 2025
a2a668b
Merge pull request #830 from topcoder-platform/pm-1494
kkartunov Jul 23, 2025
bd92a3b
Merge pull request #831 from topcoder-platform/PM-1499_edit-copilot-r…
kkartunov Jul 23, 2025
65e943e
PM-1187 - notify copilot opportunities via slack
vas3a Jul 23, 2025
624cad5
Move to reusable function
vas3a Jul 23, 2025
f79f42b
fix recipients
vas3a Jul 23, 2025
7b4edde
Merge pull request #832 from topcoder-platform/PM-1187_copilots-notif…
vas3a Jul 23, 2025
8a823f2
fix: date format
hentrymartin Jul 23, 2025
7b853be
fix: date format
hentrymartin Jul 23, 2025
682c2b3
Merge pull request #833 from topcoder-platform/pm-1494_1
hentrymartin Jul 23, 2025
9c9edce
fix: add opportunity title and type in PM emails
hentrymartin Jul 23, 2025
97becbb
fix: add opportunity title and type in PM emails
hentrymartin Jul 23, 2025
cc15920
fix: add opportunity title and type in PM emails
hentrymartin Jul 23, 2025
8218b87
Merge pull request #834 from topcoder-platform/pm-1497
hentrymartin Jul 23, 2025
25b8b34
fix: removed logic to allow inviting user even if they are already a …
hentrymartin Jul 23, 2025
7831f54
fix: allow copilots to be added even if the existing member
hentrymartin Jul 23, 2025
57a8d00
fix: allow copilots to be added even if the existing member
hentrymartin Jul 23, 2025
1b21d3b
fix: allow copilots to be added even if the existing member
hentrymartin Jul 23, 2025
fa13330
fix: allow copilots to be added even if the existing member
hentrymartin Jul 23, 2025
0536910
fix: allow copilots to be added even if the existing member
hentrymartin Jul 23, 2025
b67a6ba
fix: allow copilots to be added even if the existing member
hentrymartin Jul 23, 2025
ff30126
PM-1499 - allow PMs to fetch & edit any copilot requests
vas3a Jul 24, 2025
f38a2b6
PM-1499 - when updating copilot request, check for same type request
vas3a Jul 24, 2025
2071d45
Merge pull request #836 from topcoder-platform/PM-1499_allow-pm-to-edit
vas3a Jul 24, 2025
f26de03
fix: just switch role if user is already a member
hentrymartin Jul 24, 2025
50d2dac
fix: just switch role if user is already a member
hentrymartin Jul 24, 2025
347caae
fix: debug logs
hentrymartin Jul 24, 2025
d4e4033
fix: debug logs
hentrymartin Jul 24, 2025
ecb836c
fix: debug logs
hentrymartin Jul 24, 2025
279d2f1
fix: update kafka
hentrymartin Jul 24, 2025
3998114
fix: update kafka
hentrymartin Jul 24, 2025
b7c5f95
revert
hentrymartin Jul 24, 2025
bebe265
fix: build
hentrymartin Jul 24, 2025
b7c7f34
PM-1499 - update opportunity type & make sure canceled & fulfilled ar…
vas3a Jul 25, 2025
30ea264
Merge pull request #837 from topcoder-platform/PM-1499_allow-pm-to-edit
vas3a Jul 25, 2025
9385156
fix: added error string and already assigned role
hentrymartin Jul 25, 2025
fdb09e3
fix: added error string and already assigned role
hentrymartin Jul 25, 2025
2dc0ea2
fix: added error string and already assigned role
hentrymartin Jul 25, 2025
13c8c39
fix: added error string and already assigned role
hentrymartin Jul 25, 2025
f73f444
feat: modifications on copilot addition to project
hentrymartin Jul 27, 2025
7fce661
feat: modifications on copilot addition to project
hentrymartin Jul 27, 2025
1f2cba4
feat: modifications on copilot addition to project
hentrymartin Jul 27, 2025
1266652
feat: modifications on copilot addition to project
hentrymartin Jul 27, 2025
c701b1e
fix: complete the copilot requests if the incoming role is observer o…
hentrymartin Jul 27, 2025
1d19d15
fix: complete the copilot requests if the incoming role is observer o…
hentrymartin Jul 27, 2025
af842b0
fix: complete the copilot requests if the incoming role is observer o…
hentrymartin Jul 27, 2025
b2ac65f
correct copilots slack
kkartunov Jul 28, 2025
0d02782
fix: complete the copilot requests if the incoming role is observer o…
hentrymartin Jul 28, 2025
ce575c7
fix: action string
hentrymartin Jul 28, 2025
2f2e162
fix: send email to project manager on copilot invite acceptation
hentrymartin Jul 28, 2025
287e14f
fix: send email to project manager on copilot invite acceptation
hentrymartin Jul 28, 2025
21977ef
fix: send email to project manager on copilot invite acceptation
hentrymartin Jul 28, 2025
b4dbde5
fix: send email to project manager on copilot invite acceptation
hentrymartin Jul 28, 2025
fc0bffe
fix: send email to project manager on copilot invite acceptation
hentrymartin Jul 28, 2025
68522e0
Merge pull request #835 from topcoder-platform/pm-1506
kkartunov Jul 29, 2025
ccceb70
send email to copilot who got selected
hentrymartin Jul 29, 2025
d56f233
send email to copilot who got selected
hentrymartin Jul 29, 2025
37f740c
fix: allow action to be empty string
hentrymartin Jul 29, 2025
993ea57
fix: allow action to be empty string
hentrymartin Jul 29, 2025
b3e4f8e
fix: allow action to be empty string
hentrymartin Jul 29, 2025
45629b5
fix: allow action to be empty string
hentrymartin Jul 29, 2025
5025fa1
fix: allow action to be empty string
hentrymartin Jul 29, 2025
7075d3f
fix: allow action to be empty string
hentrymartin Jul 29, 2025
4a2a671
Merge pull request #839 from topcoder-platform/pm-1510
kkartunov Jul 30, 2025
683b3d6
added existing membership to the copilot application
hentrymartin Jul 30, 2025
f0a889f
added existing membership to the copilot application
hentrymartin Jul 30, 2025
4823330
added existing membership to the copilot application
hentrymartin Jul 30, 2025
26dc288
added existing membership to the copilot application
hentrymartin Jul 30, 2025
59c6077
added existing membership to the copilot application
hentrymartin Jul 30, 2025
2ba33ac
added existing membership to the copilot application
hentrymartin Jul 30, 2025
04b4e37
added existing membership to the copilot application
hentrymartin Jul 30, 2025
f2a8c89
added existing membership to the copilot application
hentrymartin Jul 30, 2025
76a22c1
added existing membership to the copilot application
hentrymartin Jul 30, 2025
cf120a8
added existing membership to the copilot application
hentrymartin Jul 30, 2025
471d5e1
added existing membership to the copilot application
hentrymartin Jul 30, 2025
1559e31
added existing membership to the copilot application
hentrymartin Jul 30, 2025
44ac842
added existing membership to the copilot application
hentrymartin Jul 30, 2025
de8c14f
added existing membership to the copilot application
hentrymartin Jul 30, 2025
40076d3
added existing membership to the copilot application
hentrymartin Jul 30, 2025
d40373d
added existing membership to the copilot application
hentrymartin Jul 30, 2025
8b2fa3e
added existing membership to the copilot application
hentrymartin Jul 30, 2025
e7a328b
added existing membership to the copilot application
hentrymartin Jul 30, 2025
ab268ca
added existing membership to the copilot application
hentrymartin Jul 30, 2025
e82e6b2
added existing membership to the copilot application
hentrymartin Jul 30, 2025
dad92bc
fix: show already member modal
hentrymartin Jul 30, 2025
ada10e8
fix: show already member modal
hentrymartin Jul 30, 2025
c2df9ee
fix: show already member modal
hentrymartin Jul 30, 2025
6122446
Merge pull request #840 from topcoder-platform/pm-1510
hentrymartin Jul 30, 2025
65d9a7e
deploy branch, fix sorting script
himaniraghav3 Jul 31, 2025
54bff16
Deploy - PM-1314
himaniraghav3 Jul 31, 2025
f8cf3b4
Merge pull request #841 from topcoder-platform/deploy-branch
himaniraghav3 Jul 31, 2025
ab5e2f0
PM-1314 Remove grouping logic
himaniraghav3 Jul 31, 2025
eb88f39
PM-1314 Make grouping optional
himaniraghav3 Jul 31, 2025
8587a8e
Merge pull request #842 from topcoder-platform/PM-1314
kkartunov Jul 31, 2025
8082112
fix: dont allow invite when opportunity is closed
hentrymartin Jul 31, 2025
82dee4e
fix: dont allow invite when opportunity is closed
hentrymartin Jul 31, 2025
c63ba9c
Merge pull request #843 from topcoder-platform/pm-1398
kkartunov Aug 1, 2025
b1d7b40
cancel invites on canceling the copilot opportunity
hentrymartin Aug 1, 2025
b9f8e59
Merge pull request #844 from topcoder-platform/pm-1398
hentrymartin Aug 1, 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-1378']
only: ['develop', 'migration-setup', 'pm-1398']
- deployProd:
context : org-global
filters:
Expand Down
1 change: 1 addition & 0 deletions config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"AUTH0_PROXY_SERVER_URL" : "AUTH0_PROXY_SERVER_URL",
"connectUrl": "CONNECT_URL",
"workManagerUrl": "WORK_MANAGER_URL",
"copilotsSlackEmail": "COPILOTS_SLACK_EMAIL",
"accountsAppUrl": "ACCOUNTS_APP_URL",
"inviteEmailSubject": "INVITE_EMAIL_SUBJECT",
"inviteEmailSectionTitle": "INVITE_EMAIL_SECTION_TITLE",
Expand Down
1 change: 1 addition & 0 deletions config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"workManagerUrl": "https://challenges.topcoder-dev.com",
"copilotPortalUrl": "https://copilots.topcoder-dev.com",
"accountsAppUrl": "https://accounts.topcoder-dev.com",
"copilotsSlackEmail": "[email protected]",
"MAX_REVISION_NUMBER": 100,
"UNIQUE_GMAIL_VALIDATION": false,
"pageSize": 20,
Expand Down
3 changes: 2 additions & 1 deletion config/production.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"copilotPortalUrl": "https://copilots.topcoder.com",
"sfdcBillingAccountNameField": "Billing_Account_name__c",
"sfdcBillingAccountMarkupField": "Mark_up__c",
"sfdcBillingAccountActiveField": "Active__c"
"sfdcBillingAccountActiveField": "Active__c",
"copilotsSlackEmail": "[email protected]"
}
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ export const TEMPLATE_IDS = {
APPLY_COPILOT: 'd-d7c1f48628654798a05c8e09e52db14f',
CREATE_REQUEST: 'd-3efdc91da580479d810c7acd50a4c17f',
PROJECT_MEMBER_INVITED: 'd-b47a25b103604bc28fc0ce77e77fb681',
INFORM_PM_COPILOT_APPLICATION_ACCEPTED: 'd-b35d073e302b4279a1bd208fcfe96f58',
COPILOT_ALREADY_PART_OF_PROJECT: 'd-003d41cdc9de4bbc9e14538e8f2e0585',
}
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
113 changes: 106 additions & 7 deletions src/routes/copilotOpportunity/assign.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import _ from 'lodash';
import validate from 'express-validation';
import Joi from 'joi';
import config from 'config';

import models from '../../models';
import util from '../../util';
import { PERMISSION } from '../../permissions/constants';
import { COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, PROJECT_MEMBER_ROLE, RESOURCES } from '../../constants';
import { CONNECT_NOTIFICATION_EVENT, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, PROJECT_MEMBER_ROLE, RESOURCES, TEMPLATE_IDS } from '../../constants';
import { getCopilotTypeLabel } from '../../utils/copilot';
import { createEvent } from '../../services/busApi';
import moment from 'moment';

const assignCopilotOpportunityValidations = {
body: Joi.object().keys({
Expand Down Expand Up @@ -45,11 +49,17 @@ module.exports = [
throw err;
}

const copilotRequest = await models.CopilotRequest.findOne({

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 copilotRequest might be null or undefined after the findOne query. This could prevent potential runtime errors if the expected data is not found.

where: { id: opportunity.copilotRequestId },
transaction: t,
});

const application = await models.CopilotApplication.findOne({

Choose a reason for hiding this comment

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

Ensure that the application object is properly validated before proceeding with operations that depend on its properties. This will help avoid errors if the findOne query does not return the expected result.

where: { id: applicationId, opportunityId: copilotOpportunityId },
transaction: t,
});


if (!application) {
const err = new Error('No such application available');
err.status = 400;
Expand All @@ -65,12 +75,101 @@ module.exports = [
const projectId = opportunity.projectId;
const userId = application.userId;
const activeMembers = await models.ProjectMember.getActiveProjectMembers(projectId, t);

const existingUser = activeMembers.find(item => item.userId === userId);
if (existingUser && existingUser.role === 'copilot') {
const err = new Error(`User is already a copilot of this project`);
err.status = 400;
throw err;
const updateCopilotOpportunity = async () => {
const transaction = await models.sequelize.transaction();
const memberDetails = await util.getMemberDetailsByUserIds([application.userId], req.log, req.id);
const member = memberDetails[0];
req.log.debug(`Updating opportunity: ${JSON.stringify(opportunity)}`);
await opportunity.update({
status: COPILOT_OPPORTUNITY_STATUS.COMPLETED,
}, {
transaction,
});
req.log.debug(`Updating application: ${JSON.stringify(application)}`);
await application.update({
status: COPILOT_APPLICATION_STATUS.ACCEPTED,
}, {
transaction,
});

req.log.debug(`Updating request: ${JSON.stringify(copilotRequest)}`);
await copilotRequest.update({
status: COPILOT_REQUEST_STATUS.FULFILLED,
}, {
transaction,
});

req.log.debug(`Updating other applications: ${JSON.stringify(copilotRequest)}`);

Choose a reason for hiding this comment

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

The debug log message here is misleading. It mentions updating 'other applications' but logs the copilotRequest object. Consider updating the log message to accurately reflect the action being performed.

await models.CopilotApplication.update({
status: COPILOT_APPLICATION_STATUS.CANCELED,
}, {
where: {
opportunityId: opportunity.id,
id: {
$ne: application.id,
},
}
});

Choose a reason for hiding this comment

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

Consider adding a transaction option to the CopilotApplication.update call to ensure it is part of the same transaction as the other updates.


req.log.debug(`All updations done`);
transaction.commit();

Choose a reason for hiding this comment

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

Consider using await transaction.commit() to ensure the transaction is properly awaited and any potential errors are caught.


req.log.debug(`Sending email notification`);
const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
const copilotPortalUrl = config.get('copilotPortalUrl');
const requestData = copilotRequest.data;
createEvent(emailEventType, {
data: {
opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}`,
work_manager_url: config.get('workManagerUrl'),
opportunity_type: getCopilotTypeLabel(requestData.projectType),
opportunity_title: requestData.opportunityTitle,
start_date: moment.utc(requestData.startDate).format('DD-MM-YYYY'),
user_name: member ? member.handle : "",
},
sendgrid_template_id: TEMPLATE_IDS.COPILOT_ALREADY_PART_OF_PROJECT,
recipients: [member.email],
version: 'v3',
}, req.log);

req.log.debug(`Email sent`);
};

const existingMember = activeMembers.find(item => item.userId === userId);

Choose a reason for hiding this comment

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

The variable existingMember is used here, but it is essentially the same as existingUser from the removed code. Ensure that the logic for finding the existing member is correct and consistent with the previous implementation.

if (existingMember) {
req.log.debug(`User already part of project: ${JSON.stringify(existingMember)}`);
if (['copilot', 'manager'].includes(existingMember.role)) {
req.log.debug(`User is a copilot or manager`);
await updateCopilotOpportunity();
} else {
req.log.debug(`User has read/write role`);
await models.ProjectMember.update({
role: 'copilot',
}, {
where: {
id: existingMember.id,
},
});

const projectMember = await models.ProjectMember.findOne({
where: {
id: existingMember.id,
},
});

req.log.debug(`Updated project member: ${JSON.stringify(projectMember.get({plain: true}))}`);

util.sendResourceToKafkaBus(
req,
EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED,
RESOURCES.PROJECT_MEMBER,
projectMember.get({ plain: true }),
existingMember);
req.log.debug(`Member updated in kafka`);
await updateCopilotOpportunity();
}
res.status(200).send({ id: applicationId });
return;
}

const existingInvite = await models.ProjectMemberInvite.findAll({
Expand Down
27 changes: 26 additions & 1 deletion src/routes/copilotOpportunity/delete.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import _ from 'lodash';
import { Op } from 'sequelize';
Copy link

Choose a reason for hiding this comment

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

The Op import from 'sequelize' is added but not used in the current code. Consider removing it if it's not needed to avoid unnecessary imports.


import models from '../../models';
import util from '../../util';
import { COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS } from '../../constants';
import { COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, RESOURCES } from '../../constants';
Copy link

Choose a reason for hiding this comment

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

The constants EVENT, INVITE_STATUS, and RESOURCES are imported but not used in the current code. Consider removing them if they are not needed to keep the imports clean and maintainable.

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


module.exports = [
(req, res, next) => {
if (!util.hasPermissionByReq(PERMISSION.CANCEL_COPILOT_OPPORTUNITY, req)) {
Expand Down Expand Up @@ -54,6 +56,14 @@ module.exports = [
}));
});

const allInvites = await models.ProjectMemberInvite.findAll({
Copy link

Choose a reason for hiding this comment

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

Consider adding error handling for the database query to ensure that any issues with fetching ProjectMemberInvite records are properly managed.

where: {
applicationId: {
[Op.in]: applications.map(item => item.id),
Copy link

Choose a reason for hiding this comment

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

Ensure that applications is not null or undefined before calling map on it to prevent potential runtime errors.

},
},
});

await Promise.all(promises);

await copilotRequest.update({
Expand All @@ -68,6 +78,21 @@ module.exports = [
transaction,
});

// update all the existing invites which are
// associated to the copilot opportunity
// with cancel status
for (const invite of allInvites) {
await invite.update({
Copy link

Choose a reason for hiding this comment

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

Consider adding error handling for the update operation to ensure that any issues during the update process are caught and managed appropriately.

status: INVITE_STATUS.CANCELED,
});
await invite.reload();
Copy link

Choose a reason for hiding this comment

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

Consider adding error handling for the reload operation to ensure that any issues during the reload process are caught and managed appropriately.

util.sendResourceToKafkaBus(
Copy link

Choose a reason for hiding this comment

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

Consider adding error handling for the sendResourceToKafkaBus function to ensure that any issues during the message sending process are caught and managed appropriately.

req,
EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_UPDATED,
RESOURCES.PROJECT_MEMBER_INVITE,
invite.toJSON());
}

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

Expand Down
21 changes: 20 additions & 1 deletion src/routes/copilotOpportunity/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,25 @@ module.exports = [
const pageSize = parseInt(req.query.pageSize, 10) || DEFAULT_PAGE_SIZE;
const offset = (page - 1) * pageSize;
const limit = pageSize;
const noGroupingByStatus = req.query.noGrouping === 'true';

Choose a reason for hiding this comment

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

Consider using a more descriptive variable name than noGroupingByStatus to improve code readability. For example, disableStatusGrouping might better convey the purpose of the variable.


const baseOrder = [];

// If grouping is enabled (default), add custom ordering based on status
if (!noGroupingByStatus) {
baseOrder.push([
models.Sequelize.literal(`
CASE
WHEN "CopilotOpportunity"."status" = 'active' THEN 0
WHEN "CopilotOpportunity"."status" = 'cancelled' THEN 1
WHEN "CopilotOpportunity"."status" = 'completed' THEN 2
ELSE 3
END
`),
'ASC',
]);
}
baseOrder.push([sortParams[0], sortParams[1]]);

Choose a reason for hiding this comment

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

Ensure sortParams is validated before being used in baseOrder.push([sortParams[0], sortParams[1]]); to prevent potential runtime errors if sortParams is not an array or does not contain the expected elements.


return models.CopilotOpportunity.findAll({
include: [
Expand All @@ -34,7 +53,7 @@ module.exports = [
attributes: ['name'],
},
],
order: [[sortParams[0], sortParams[1]]],
order: baseOrder,
limit,
offset,
})
Expand Down
12 changes: 11 additions & 1 deletion src/routes/copilotOpportunityApply/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import util from '../../util';
import { PERMISSION } from '../../permissions/constants';
import { CONNECT_NOTIFICATION_EVENT, COPILOT_OPPORTUNITY_STATUS, TEMPLATE_IDS, USER_ROLE } from '../../constants';
import { createEvent } from '../../services/busApi';
import { getCopilotTypeLabel } from '../../utils/copilot';

const applyCopilotRequestValidations = {
body: Joi.object().keys({
notes: Joi.string().optional(),
notes: Joi.string().required(),

Choose a reason for hiding this comment

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

The notes field has been changed from optional to required. Ensure that all parts of the application that interact with this endpoint are updated to handle this change, as it may cause validation errors if notes is not provided.

}),
};

Expand Down Expand Up @@ -41,6 +42,12 @@ module.exports = [
where: {
id: copilotOpportunityId,
},
include: [

Choose a reason for hiding this comment

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

Consider verifying that the models.CopilotRequest is correctly defined and imported in this file to avoid runtime errors.

{
model: models.CopilotRequest,
as: 'copilotRequest',
},
],
}).then(async (opportunity) => {
if (!opportunity) {
const err = new Error('No opportunity found');
Expand Down Expand Up @@ -92,12 +99,15 @@ module.exports = [

const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL;
const copilotPortalUrl = config.get('copilotPortalUrl');
const requestData = opportunity.copilotRequest.data;

Choose a reason for hiding this comment

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

Consider checking if opportunity.copilotRequest and opportunity.copilotRequest.data are defined before accessing requestData. This will prevent potential runtime errors if these properties are undefined.

listOfSubjects.forEach((subject) => {
createEvent(emailEventType, {
data: {
user_name: subject.handle,
opportunity_details_url: `${copilotPortalUrl}/opportunity/${opportunity.id}#applications`,
work_manager_url: config.get('workManagerUrl'),
opportunity_type: getCopilotTypeLabel(requestData.projectType),

Choose a reason for hiding this comment

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

Ensure that getCopilotTypeLabel(requestData.projectType) handles all possible values of projectType and returns a valid label. Consider adding validation or a default case to handle unexpected values.

opportunity_title: requestData.opportunityTitle,

Choose a reason for hiding this comment

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

Consider validating requestData.opportunityTitle to ensure it is not null or undefined before using it. This will help avoid potential issues with missing data.

},
sendgrid_template_id: TEMPLATE_IDS.APPLY_COPILOT,
recipients: [subject.email],
Expand Down
75 changes: 62 additions & 13 deletions src/routes/copilotOpportunityApply/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,68 @@ module.exports = [
canAccessAllApplications ? {} : { createdBy: userId },
);

return models.CopilotApplication.findAll({
where: whereCondition,
include: [
{
model: models.CopilotOpportunity,
as: 'copilotOpportunity',
},
],
order: [[sortParams[0], sortParams[1]]],
return models.CopilotOpportunity.findOne({
where: {
id: opportunityId,
}
}).then((opportunity) => {

Choose a reason for hiding this comment

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

Consider adding a timeout or handling for the promise returned by models.CopilotOpportunity.findOne to prevent potential hanging if the database query takes too long.

if (!opportunity) {
const err = new Error('No opportunity found');

Choose a reason for hiding this comment

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

The error message 'No opportunity found' could be more descriptive by including the opportunityId to help with debugging.

err.status = 404;
throw err;
}
return models.CopilotApplication.findAll({
where: whereCondition,
include: [
{
model: models.CopilotOpportunity,
as: 'copilotOpportunity',
},
],
order: [[sortParams[0], sortParams[1]]],
})
.then(copilotApplications => {
req.log.debug(`CopilotApplications ${JSON.stringify(copilotApplications)}`);
return models.ProjectMember.getActiveProjectMembers(opportunity.projectId).then((members) => {

Choose a reason for hiding this comment

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

Consider adding error handling for the promise returned by models.ProjectMember.getActiveProjectMembers to ensure that any issues fetching project members are properly managed.

req.log.debug(`Fetched existing active members ${JSON.stringify(members)}`);
req.log.debug(`Applications ${JSON.stringify(copilotApplications)}`);
const enrichedApplications = copilotApplications.map(application => {
const m = members.find(m => m.userId === application.userId);

Choose a reason for hiding this comment

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

The variable m could be renamed to something more descriptive, such as member, to improve code readability.


// Using spread operator fails in lint check
// While Object.assign fails silently during run time
// So using this method
const enriched = {
id: application.id,
opportunityId: application.opportunityId,
notes: application.notes,
status: application.status,
userId: application.userId,
deletedAt: application.deletedAt,
createdAt: application.createdAt,
updatedAt: application.updatedAt,
deletedBy: application.deletedBy,
createdBy: application.createdBy,
updatedBy: application.updatedBy,
copilotOpportunity: application.copilotOpportunity,
};

if (m) {
enriched.existingMembership = m;
}

req.log.debug(`Existing member to application ${JSON.stringify(enriched)}`);

return enriched;
});

req.log.debug(`Enriched Applications ${JSON.stringify(enrichedApplications)}`);

Choose a reason for hiding this comment

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

Consider using res.json(enrichedApplications) instead of res.status(200).send(enrichedApplications) for consistency with the previous implementation and to automatically set the correct content-type header.

res.status(200).send(enrichedApplications);
});
})
})
.then(copilotApplications => res.json(copilotApplications))
.catch((err) => {
util.handleError('Error fetching copilot applications', err, req, next);
});
.catch((err) => {
util.handleError('Error fetching copilot applications', err, req, next);
});
},
];
Loading