Skip to content

fix(PM-1510): added existing membership to the copilot application #840

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

Merged
merged 23 commits into from
Jul 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
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
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';

Choose a reason for hiding this comment

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

The config import is added but not used in the current diff. Ensure that it is necessary, or remove it if it is not used.


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';

Choose a reason for hiding this comment

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

The getCopilotTypeLabel import is added but not used in the current diff. Ensure that it is necessary, or remove it if it is not used.

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.

The import statement for createEvent from ../../services/busApi is added, but it is not used in the current code. Consider removing it if it is not needed, or ensure it is utilized in the code changes.

import moment from 'moment';

Choose a reason for hiding this comment

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

Consider using a more lightweight date library or native JavaScript Date methods if only basic date manipulation is required. The 'moment' library can significantly increase the bundle size.


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 checking if copilotRequest is null or undefined after fetching it from the database. This will help prevent potential runtime errors if the request is not found.

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

const application = await models.CopilotApplication.findOne({
where: { id: applicationId, opportunityId: copilotOpportunityId },
transaction: t,
});


Choose a reason for hiding this comment

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

Remove the extra blank line here to maintain consistent formatting and readability.

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();

Choose a reason for hiding this comment

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

The transaction is initialized here but not used in a try-catch block. Consider wrapping the transaction logic in a try-catch block to ensure that the transaction is properly committed or rolled back in case of an error.

const memberDetails = await util.getMemberDetailsByUserIds([application.userId], req.log, req.id);

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 getMemberDetailsByUserIds function call to manage cases where the function might fail or return an unexpected result.

const member = memberDetails[0];

Choose a reason for hiding this comment

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

Ensure that memberDetails contains at least one element before accessing memberDetails[0] to prevent potential runtime errors.

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 log message here seems to be incorrect. It mentions copilotRequest but the operation is updating other applications. Consider updating the log message to reflect the correct context, such as Updating other applications status to CANCELED.

await models.CopilotApplication.update({
status: COPILOT_APPLICATION_STATUS.CANCELED,
}, {
where: {
opportunityId: opportunity.id,

Choose a reason for hiding this comment

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

The removal of projectId from the where clause may affect the specificity of the query. Ensure that this change is intentional and that the query will still correctly identify the intended records without this condition.

id: {
$ne: application.id,
},
}
});

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

Choose a reason for hiding this comment

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

Consider removing or rephrasing the debug log message to be more specific about what updates were completed. This will help in understanding the context of the log when reviewing logs later.

transaction.commit();

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 transaction.commit() operation to ensure that any issues during the commit process are properly managed. This could involve wrapping the commit in a try-catch block and handling any potential exceptions.


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, {

Choose a reason for hiding this comment

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

Consider adding error handling around the createEvent function to ensure that any issues with sending the email do not affect the transaction commit.

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],

Choose a reason for hiding this comment

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

Ensure that member.email is validated or sanitized before being used as a recipient to prevent potential issues with invalid email addresses.

version: 'v3',
}, req.log);

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

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

Choose a reason for hiding this comment

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

The logic for checking existingMember and its role could be optimized. Consider combining the role check with the initial find operation to reduce complexity and improve readability.

req.log.debug(`User already part of project: ${JSON.stringify(existingMember)}`);

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 JSON.stringify(existingMember) might throw an error if existingMember contains circular references. You could use a try-catch block or a safe stringification method.

if (['copilot', 'manager'].includes(existingMember.role)) {
req.log.debug(`User is a copilot or manager`);

Choose a reason for hiding this comment

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

The debug log message User is a copilot or manager could be more descriptive by including the user's role. Consider logging the actual role for better traceability.

await updateCopilotOpportunity();

Choose a reason for hiding this comment

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

The function updateCopilotOpportunity is now awaited, which implies it returns a promise. Ensure that the function is properly handling asynchronous operations and that any necessary error handling is in place for the promise resolution or rejection.

} else {
req.log.debug(`User has read/write role`);

Choose a reason for hiding this comment

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

The debug log message User has read/write role might be misleading if the user has other roles. Consider specifying the exact role or roles the user has for clarity.

await models.ProjectMember.update({

Choose a reason for hiding this comment

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

The await keyword is used here, but the surrounding code does not handle potential errors that might arise from this asynchronous operation. Consider adding error handling to manage any exceptions that may occur.

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}))}`);

Choose a reason for hiding this comment

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

Consider handling potential errors that might occur during the JSON.stringify operation. If projectMember.get({plain: true}) contains circular references or other non-serializable data, JSON.stringify could throw an error.


util.sendResourceToKafkaBus(
req,
EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED,
RESOURCES.PROJECT_MEMBER,
projectMember.get({ plain: true }),
existingMember);
req.log.debug(`Member updated in kafka`);

Choose a reason for hiding this comment

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

The log message Member updated in kafka could be more descriptive. Consider including additional context, such as the member ID or other relevant details, to make the log more informative.

await updateCopilotOpportunity();

Choose a reason for hiding this comment

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

The function updateCopilotOpportunity() is now awaited. Ensure that this function returns a promise, otherwise the await keyword will not have the intended effect. If it does not return a promise, consider refactoring the function to do so.

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

const existingInvite = await models.ProjectMemberInvite.findAll({
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 },
);

Choose a reason for hiding this comment

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

The opportunityId variable is used here, but it is not clear from the diff where it is defined or assigned. Ensure that opportunityId is properly defined and assigned a value before this line.

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) => {
if (!opportunity) {
const err = new Error('No opportunity found');
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)}`);

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 log message to clarify what copilotApplications represents in this context.

return models.ProjectMember.getActiveProjectMembers(opportunity.projectId).then((members) => {
req.log.debug(`Fetched existing active members ${JSON.stringify(members)}`);
req.log.debug(`Applications ${JSON.stringify(copilotApplications)}`);

Choose a reason for hiding this comment

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

The log message for Applications is redundant with the previous log message for CopilotApplications. Consider removing one of them to avoid unnecessary duplication.

const enrichedApplications = copilotApplications.map(application => {

Choose a reason for hiding this comment

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

Consider using const for enrichedApplications only if it is not reassigned later in the code. If it is reassigned, let would be more appropriate.

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 debug log statement was removed but the comment remains. Consider removing the comment as it is not necessary to keep commented-out code.


// Using spread operator fails in lint check

Choose a reason for hiding this comment

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

Consider removing the commented-out code if it is no longer needed to keep the codebase clean.

// While Object.assign fails silently during run time
// So using this method
const enriched = {
id: application.id,

Choose a reason for hiding this comment

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

Instead of manually listing all properties, consider using a utility function or library to clone objects if the goal is to avoid using Object.assign or the spread operator. This can help reduce potential errors and improve maintainability.

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.

The debug log message could be more informative by including the number of enriched applications. Consider adding enrichedApplications.length to the log message.

res.status(200).send(enrichedApplications);

Choose a reason for hiding this comment

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

The response method has been changed from res.json to res.status(200).send. Ensure that this change is intentional and that send is the appropriate method for sending the enriched applications. If res.json was previously used to automatically set the content-type to application/json, verify that send achieves the same result.

});
})
})
.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);
});
},
];
6 changes: 2 additions & 4 deletions src/routes/projectMembers/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,6 @@ const completeAllCopilotRequests = async (req, projectId, _transaction, _member)

req.log.debug(`Sent email to ${member.email}`);
});

await _transaction.commit();
};

Choose a reason for hiding this comment

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

The removal of _transaction.commit() may lead to uncommitted transactions if this function is intended to finalize database changes. Ensure that the transaction is committed elsewhere if this is intentional, or consider re-adding the commit statement to prevent potential data inconsistencies.

module.exports = [
Expand Down Expand Up @@ -263,8 +261,8 @@ module.exports = [
projectMember = projectMember.get({ plain: true });
projectMember = _.omit(projectMember, ['deletedAt']);

if (['observer', 'customer'].includes(updatedProps.role)) {
await completeAllCopilotRequests(req, projectId, _transaction, _member);
if (['observer', 'customer'].includes(previousValue.role) && ['copilot', 'manager'].includes(updatedProps.role)) {

Choose a reason for hiding this comment

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

Consider checking if updatedProps.role is defined before using it in the condition to prevent potential runtime errors if updatedProps is undefined or does not have a role property.

await completeAllCopilotRequests(req, projectId, _transaction, projectMember);

Choose a reason for hiding this comment

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

The variable _member was replaced with projectMember. Ensure that projectMember contains all necessary properties and data expected by the completeAllCopilotRequests function. Verify that this change aligns with the intended functionality and does not introduce any unintended side effects.

}
})
.then(() => (
Expand Down