From aeb7b48531211679b9b697359eaff9016ceb688e Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Sun, 11 May 2025 16:17:58 +0200 Subject: [PATCH 1/9] feat: assign copilot opportunity --- docs/swagger.yaml | 45 +++++++++ ...250511123109-copilot_application_status.js | 21 +++++ src/constants.js | 5 + src/models/copilotApplication.js | 10 ++ src/permissions/constants.js | 12 +++ src/routes/copilotOpportunity/assign.js | 94 +++++++++++++++++++ src/routes/index.js | 4 + 7 files changed, 191 insertions(+) create mode 100644 migrations/umzug/migrations/20250511123109-copilot_application_status.js create mode 100644 src/routes/copilotOpportunity/assign.js diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ae0cd049..83931b5e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -560,6 +560,37 @@ paths: description: "Internal Server Error" schema: $ref: "#/definitions/ErrorModel" + "/projects/copilots/opportunity/{copilotOpportunityId}/assign": + post: + tags: + - assign project copilot opportunity + operationId: assignCopilotOpportunity + security: + - Bearer: [] + description: "Retrieve a specific copilot opportunity." + parameters: + - $ref: "#/parameters/copilotOpportunityIdParam" + - in: body + name: body + schema: + $ref: "#/definitions/AssignCopilotOpportunity" + responses: + "200": + description: "The copilot opportunity application" + schema: + $ref: "#/definitions/CopilotOpportunityApplication" + "401": + description: "Unauthorized" + schema: + $ref: "#/definitions/ErrorModel" + "403": + description: "Forbidden - User does not have permission" + schema: + $ref: "#/definitions/ErrorModel" + "500": + description: "Internal Server Error" + schema: + $ref: "#/definitions/ErrorModel" "/projects/{projectId}/attachments": get: tags: @@ -6081,6 +6112,13 @@ definitions: notes: description: notes regarding the application type: string + status: + description: status of the application + type: string + enum: + - pending + - accepted + example: pending opportunityId: description: copilot request id type: integer @@ -6327,6 +6365,13 @@ definitions: notes: type: string description: notes about applying copilot opportunity + AssignCopilotOpportunity: + title: Assign copilot CopilotOpportunity + type: object + properties: + applicationId: + type: string + description: application id which has to be accepted NewProjectAttachment: title: Project attachment request type: object diff --git a/migrations/umzug/migrations/20250511123109-copilot_application_status.js b/migrations/umzug/migrations/20250511123109-copilot_application_status.js new file mode 100644 index 00000000..bbac7d2c --- /dev/null +++ b/migrations/umzug/migrations/20250511123109-copilot_application_status.js @@ -0,0 +1,21 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('copilot_applications', 'status', { + type: Sequelize.STRING(16), + allowNull: true, + }); + + await queryInterface.sequelize.query( + `UPDATE copilot_applications SET status = 'pending' WHERE status IS NULL` + ); + + await queryInterface.changeColumn('copilot_applications', 'status', { + type: Sequelize.STRING(16), + allowNull: false, + }); + }, + + down: async (queryInterface) => { + await queryInterface.removeColumn('copilot_applications', 'status'); + }, +}; diff --git a/src/constants.js b/src/constants.js index 8307b773..d432c1c5 100644 --- a/src/constants.js +++ b/src/constants.js @@ -18,6 +18,11 @@ export const COPILOT_REQUEST_STATUS = { FULFILLED: 'fulfiled', }; +export const COPILOT_APPLICATION_STATUS = { + PENDING: 'pending', + ACCEPTED: 'accepted', +}; + export const COPILOT_OPPORTUNITY_STATUS = { ACTIVE: 'active', COMPLETED: 'completed', diff --git a/src/models/copilotApplication.js b/src/models/copilotApplication.js index c472da60..e8298125 100644 --- a/src/models/copilotApplication.js +++ b/src/models/copilotApplication.js @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { COPILOT_APPLICATION_STATUS } from '../constants'; module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { const CopilotApplication = sequelize.define('CopilotApplication', { @@ -17,6 +18,15 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { type: DataTypes.TEXT, allowNull: true }, + status: { + type: DataTypes.STRING(16), + defaultValue: 'pending', + allowNull: false, + validate: { + isIn: [_.values(COPILOT_APPLICATION_STATUS)], + }, + allowNull: false, + }, userId: { type: DataTypes.BIGINT, allowNull: false }, deletedAt: { type: DataTypes.DATE, allowNull: true }, createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, diff --git a/src/permissions/constants.js b/src/permissions/constants.js index 03bcaf21..c32e16d4 100644 --- a/src/permissions/constants.js +++ b/src/permissions/constants.js @@ -276,6 +276,18 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export ], scopes: SCOPES_PROJECTS_WRITE, }, + ASSIGN_COPILOT_OPPORTUNITY: { + meta: { + title: 'Assign copilot to opportunity', + group: 'Assign Copilot', + description: 'Who can assign for copilot opportunity.', + }, + topcoderRoles: [ + USER_ROLE.PROJECT_MANAGER, + USER_ROLE.TOPCODER_ADMIN, + ], + scopes: SCOPES_PROJECTS_WRITE, + }, LIST_COPILOT_OPPORTUNITY: { meta: { diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js new file mode 100644 index 00000000..87803a5a --- /dev/null +++ b/src/routes/copilotOpportunity/assign.js @@ -0,0 +1,94 @@ +import _ from 'lodash'; +import validate from 'express-validation'; +import Joi from 'joi'; + +import models from '../../models'; +import util from '../../util'; +import { PERMISSION } from '../../permissions/constants'; +import { COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS } from '../../constants'; + +const assignCopilotOpportunityValidations = { + body: Joi.object().keys({ + applicationId: Joi.string(), + }), +}; + +module.exports = [ + validate(assignCopilotOpportunityValidations), + async (req, res, next) => { + const { applicationId } = req.body; + const copilotOpportunityId = _.parseInt(req.params.id); + if (!util.hasPermissionByReq(PERMISSION.ASSIGN_COPILOT_OPPORTUNITY, req)) { + const err = new Error('Unable to assign copilot opportunity'); + _.assign(err, { + details: JSON.stringify({ message: 'You do not have permission to assign a copilot opportunity' }), + status: 403, + }); + return next(err); + } + + return models.sequelize.transaction(() => { + models.CopilotOpportunity.findOne({ + where: { + id: copilotOpportunityId, + }, + }).then(async (opportunity) => { + if (!opportunity) { + const err = new Error('No opportunity found'); + err.status = 404; + return next(err); + } + + if (opportunity.status !== COPILOT_OPPORTUNITY_STATUS.ACTIVE) { + const err = new Error('Opportunity is not active'); + err.status = 400; + return next(err); + } + + const application = models.CopilotApplication.findOne({ + where: { + id: applicationId, + }, + }); + + if (!application) { + const err = new Error('No such application available'); + err.status = 400; + return next(err); + } + + if (application.status === COPILOT_APPLICATION_STATUS.ACCEPTED) { + const err = new Error('Application already accepted'); + err.status = 400; + return next(err); + } + + const projectId = opportunity.projectId; + const userId = application.userId; + + const activeMembers = await models.ProjectMember.getActiveProjectMembers(projectId); + + const existingUser = activeMembers.find(item => item.userId === userId); + + if (existingUser) { + const err = new Error(`User is already part of the project as ${existingUser.role}`); + err.status = 400; + return next(err); + } + + + return opportunity.update({status: COPILOT_OPPORTUNITY_STATUS.COMPLETED}) + }) + .then(() => { + return models.CopilotApplication.update({ + status: 'ACCEPTED' + }, { + where: { + id: applicationId, + } + }) + }) + }) + .catch(err => next(err)); + }, +]; diff --git a/src/routes/index.js b/src/routes/index.js index b07041ab..01b2d1e6 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -410,6 +410,10 @@ router.route('/v5/projects/copilots/opportunity/:id(\\d+)/apply') router.route('/v5/projects/copilots/opportunity/:id(\\d+)/applications') .get(require('./copilotOpportunityApply/list')); +// Copilot opportunity assign +router.route('/v5/projects/copilots/opportunity/:id(\\d+)/assign') + .post(require('./copilotOpportunity/assign')); + // Project Estimation Items router.route('/v5/projects/:projectId(\\d+)/estimations/:estimationId(\\d+)/items') .get(require('./projectEstimationItems/list')); From af846ab660c3bbd8f8bcc968ac29cd48d878e7fe Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Sun, 11 May 2025 16:18:35 +0200 Subject: [PATCH 2/9] deploy feature branch to dev --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1acd4a4c..90930d0d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup'] + only: ['develop', 'migration-setup', 'pm-1168'] - deployProd: context : org-global filters: From 709601f1d8bc67e05e32f8186ab09a47ef2bff8c Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Sun, 11 May 2025 17:48:39 +0200 Subject: [PATCH 3/9] deploy feature branch to dev --- docs/swagger.yaml | 8 ++++---- src/models/copilotApplication.js | 1 - src/routes/copilotOpportunity/assign.js | 9 ++++++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 83931b5e..92663901 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -567,7 +567,7 @@ paths: operationId: assignCopilotOpportunity security: - Bearer: [] - description: "Retrieve a specific copilot opportunity." + description: "Assign a copilot opportunity with copilot." parameters: - $ref: "#/parameters/copilotOpportunityIdParam" - in: body @@ -6359,19 +6359,19 @@ definitions: - manager - copilot ApplyCopilotOpportunity: - title: Apply copilot CopilotOpportunity + title: Apply Copilot Opportunity type: object properties: notes: type: string description: notes about applying copilot opportunity AssignCopilotOpportunity: - title: Assign copilot CopilotOpportunity + title: Assign Copilot Opportunity type: object properties: applicationId: type: string - description: application id which has to be accepted + description: The ID of the application to be accepted for the copilot opportunity. NewProjectAttachment: title: Project attachment request type: object diff --git a/src/models/copilotApplication.js b/src/models/copilotApplication.js index e8298125..f68e6c60 100644 --- a/src/models/copilotApplication.js +++ b/src/models/copilotApplication.js @@ -21,7 +21,6 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { status: { type: DataTypes.STRING(16), defaultValue: 'pending', - allowNull: false, validate: { isIn: [_.values(COPILOT_APPLICATION_STATUS)], }, diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index 87803a5a..40f13793 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -80,13 +80,16 @@ module.exports = [ return opportunity.update({status: COPILOT_OPPORTUNITY_STATUS.COMPLETED}) }) .then(() => { - return models.CopilotApplication.update({ - status: 'ACCEPTED' + const updatedApplication = models.CopilotApplication.update({ + status: COPILOT_APPLICATION_STATUS.ACCEPTED, }, { where: { id: applicationId, } - }) + }); + + res.status(200).send(updatedApplication); + return Promise.resolve() }) }) .catch(err => next(err)); From 4543e5e49e927a9232b39bdb5c94640fd5912036 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 12 May 2025 19:30:52 +0200 Subject: [PATCH 4/9] fix: return object --- src/routes/copilotOpportunity/assign.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index 40f13793..a889c5c7 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -79,8 +79,8 @@ module.exports = [ return opportunity.update({status: COPILOT_OPPORTUNITY_STATUS.COMPLETED}) }) - .then(() => { - const updatedApplication = models.CopilotApplication.update({ + .then(async () => { + const [, affected] = await models.CopilotApplication.update({ status: COPILOT_APPLICATION_STATUS.ACCEPTED, }, { where: { @@ -88,7 +88,7 @@ module.exports = [ } }); - res.status(200).send(updatedApplication); + res.status(200).send(affected[0]); return Promise.resolve() }) }) From 9f7dc4804c2608d4fef08255895c2f9dfdaf1280 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 12 May 2025 19:54:14 +0200 Subject: [PATCH 5/9] fix: return object --- src/routes/copilotOpportunity/assign.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index a889c5c7..213d4cfc 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -80,7 +80,7 @@ module.exports = [ return opportunity.update({status: COPILOT_OPPORTUNITY_STATUS.COMPLETED}) }) .then(async () => { - const [, affected] = await models.CopilotApplication.update({ + const updated = await models.CopilotApplication.update({ status: COPILOT_APPLICATION_STATUS.ACCEPTED, }, { where: { @@ -88,7 +88,7 @@ module.exports = [ } }); - res.status(200).send(affected[0]); + res.status(200).send(updated); return Promise.resolve() }) }) From 7f40cdb1cd75594150f26ae410c7fd914deb95a7 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 12 May 2025 20:33:23 +0200 Subject: [PATCH 6/9] fix: return object --- docs/swagger.yaml | 9 ++++++++- src/routes/copilotOpportunity/assign.js | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 92663901..5ac22e74 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -578,7 +578,7 @@ paths: "200": description: "The copilot opportunity application" schema: - $ref: "#/definitions/CopilotOpportunityApplication" + $ref: "#/definitions/CopilotOpportunityAssignResponse" "401": description: "Unauthorized" schema: @@ -6149,6 +6149,13 @@ definitions: format: int64 description: READ-ONLY. User that deleted this task readOnly: true + CopilotOpportunityAssignResponse: + type: object + properties: + id: + description: unique identifier + type: integer + format: int64 Project: type: object properties: diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index 213d4cfc..f41026ee 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -80,7 +80,7 @@ module.exports = [ return opportunity.update({status: COPILOT_OPPORTUNITY_STATUS.COMPLETED}) }) .then(async () => { - const updated = await models.CopilotApplication.update({ + await models.CopilotApplication.update({ status: COPILOT_APPLICATION_STATUS.ACCEPTED, }, { where: { @@ -88,7 +88,7 @@ module.exports = [ } }); - res.status(200).send(updated); + res.status(200).send({id: applicationId}); return Promise.resolve() }) }) From 8e2881d3016141eb5e9b6749cc922de7debe4d01 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 12 May 2025 20:56:29 +0200 Subject: [PATCH 7/9] updated swagger docs --- docs/swagger.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5ac22e74..60fe8f75 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -576,7 +576,7 @@ paths: $ref: "#/definitions/AssignCopilotOpportunity" responses: "200": - description: "The copilot opportunity application" + description: "The response after assigning an copilot opportunity" schema: $ref: "#/definitions/CopilotOpportunityAssignResponse" "401": From 2ee4f9b258e75dc82e6113f3a51aba7236c33bf5 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 13 May 2025 23:18:51 +0200 Subject: [PATCH 8/9] fix: lint --- ...0250411182312-copilot_opportunity_apply.js | 4 +- ...250511123109-copilot_application_status.js | 2 +- src/models/copilotApplication.js | 6 +- src/permissions/copilotApplications.view.js | 9 +- src/routes/copilotOpportunity/assign.js | 111 +++++++++--------- src/routes/copilotOpportunityApply/create.js | 8 +- src/routes/copilotOpportunityApply/list.js | 3 +- src/routes/projectMemberInvites/create.js | 13 +- 8 files changed, 74 insertions(+), 82 deletions(-) diff --git a/migrations/umzug/migrations/20250411182312-copilot_opportunity_apply.js b/migrations/umzug/migrations/20250411182312-copilot_opportunity_apply.js index 7d29919e..27910a56 100644 --- a/migrations/umzug/migrations/20250411182312-copilot_opportunity_apply.js +++ b/migrations/umzug/migrations/20250411182312-copilot_opportunity_apply.js @@ -1,4 +1,4 @@ -'use strict'; + module.exports = { up: async (queryInterface, Sequelize) => { @@ -56,5 +56,5 @@ module.exports = { down: async (queryInterface) => { await queryInterface.dropTable('copilot_applications'); - } + }, }; diff --git a/migrations/umzug/migrations/20250511123109-copilot_application_status.js b/migrations/umzug/migrations/20250511123109-copilot_application_status.js index bbac7d2c..2c03606b 100644 --- a/migrations/umzug/migrations/20250511123109-copilot_application_status.js +++ b/migrations/umzug/migrations/20250511123109-copilot_application_status.js @@ -6,7 +6,7 @@ module.exports = { }); await queryInterface.sequelize.query( - `UPDATE copilot_applications SET status = 'pending' WHERE status IS NULL` + 'UPDATE copilot_applications SET status = \'pending\' WHERE status IS NULL', ); await queryInterface.changeColumn('copilot_applications', 'status', { diff --git a/src/models/copilotApplication.js b/src/models/copilotApplication.js index f68e6c60..9a90881f 100644 --- a/src/models/copilotApplication.js +++ b/src/models/copilotApplication.js @@ -9,14 +9,14 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { allowNull: false, references: { model: 'copilot_opportunities', - key: 'id' + key: 'id', }, onUpdate: 'CASCADE', - onDelete: 'CASCADE' + onDelete: 'CASCADE', }, notes: { type: DataTypes.TEXT, - allowNull: true + allowNull: true, }, status: { type: DataTypes.STRING(16), diff --git a/src/permissions/copilotApplications.view.js b/src/permissions/copilotApplications.view.js index 9b0c917b..975846bb 100644 --- a/src/permissions/copilotApplications.view.js +++ b/src/permissions/copilotApplications.view.js @@ -25,11 +25,9 @@ module.exports = freq => new Promise((resolve, reject) => { const isProjectManager = util.hasProjectManagerRole(req); return models.ProjectMember.getActiveProjectMembers(projectId) - .then((members) => { - - return models.CopilotApplication.findOne({ + .then(members => models.CopilotApplication.findOne({ where: { - opportunityId: opportunityId, + opportunityId, userId: currentUserId, }, }).then((copilotApplication) => { @@ -37,8 +35,7 @@ module.exports = freq => new Promise((resolve, reject) => { // check if auth user has access to this project const hasAccess = util.hasAdminRole(req) || isPartOfProject || !!copilotApplication; return Promise.resolve(hasAccess); - }) - }) + })); }) .then((hasAccess) => { if (!hasAccess) { diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index f41026ee..be588ce3 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -5,7 +5,7 @@ import Joi from 'joi'; import models from '../../models'; import util from '../../util'; import { PERMISSION } from '../../permissions/constants'; -import { COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS } from '../../constants'; +import { COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS } from '../../constants'; const assignCopilotOpportunityValidations = { body: Joi.object().keys({ @@ -27,71 +27,68 @@ module.exports = [ return next(err); } - return models.sequelize.transaction(() => { - models.CopilotOpportunity.findOne({ - where: { - id: copilotOpportunityId, - }, - }).then(async (opportunity) => { - if (!opportunity) { - const err = new Error('No opportunity found'); - err.status = 404; - return next(err); - } - - if (opportunity.status !== COPILOT_OPPORTUNITY_STATUS.ACTIVE) { - const err = new Error('Opportunity is not active'); - err.status = 400; - return next(err); - } + return models.sequelize.transaction(async (t) => { + const opportunity = await models.CopilotOpportunity.findOne({ + where: { id: copilotOpportunityId }, + transaction: t, + }); + + if (!opportunity) { + const err = new Error('No opportunity found'); + err.status = 404; + throw err; + } - const application = models.CopilotApplication.findOne({ - where: { - id: applicationId, - }, - }); + if (opportunity.status !== COPILOT_OPPORTUNITY_STATUS.ACTIVE) { + const err = new Error('Opportunity is not active'); + err.status = 400; + throw err; + } - if (!application) { - const err = new Error('No such application available'); - err.status = 400; - return next(err); - } + const application = await models.CopilotApplication.findOne({ + where: { id: applicationId }, + transaction: t, + }); - if (application.status === COPILOT_APPLICATION_STATUS.ACCEPTED) { - const err = new Error('Application already accepted'); - err.status = 400; - return next(err); - } + if (!application) { + const err = new Error('No such application available'); + err.status = 400; + throw err; + } - const projectId = opportunity.projectId; - const userId = application.userId; + if (application.status === COPILOT_APPLICATION_STATUS.ACCEPTED) { + const err = new Error('Application already accepted'); + err.status = 400; + throw err; + } - const activeMembers = await models.ProjectMember.getActiveProjectMembers(projectId); + const projectId = opportunity.projectId; + const userId = application.userId; + const activeMembers = await models.ProjectMember.getActiveProjectMembers(projectId); - const existingUser = activeMembers.find(item => item.userId === userId); + const existingUser = activeMembers.find(item => item.userId === userId); + if (existingUser) { + const err = new Error(`User is already part of the project as ${existingUser.role}`); + err.status = 400; + throw err; + } - if (existingUser) { - const err = new Error(`User is already part of the project as ${existingUser.role}`); - err.status = 400; - return next(err); - } + await models.CopilotRequest.update( + { status: COPILOT_REQUEST_STATUS.FULFILLED }, + { where: { id: opportunity.copilotRequestId }, transaction: t }, + ); + await opportunity.update( + { status: COPILOT_OPPORTUNITY_STATUS.COMPLETED }, + { transaction: t }, + ); - return opportunity.update({status: COPILOT_OPPORTUNITY_STATUS.COMPLETED}) - }) - .then(async () => { - await models.CopilotApplication.update({ - status: COPILOT_APPLICATION_STATUS.ACCEPTED, - }, { - where: { - id: applicationId, - } - }); + await models.CopilotApplication.update( + { status: COPILOT_APPLICATION_STATUS.ACCEPTED }, + { where: { id: applicationId }, transaction: t }, + ); - res.status(200).send({id: applicationId}); - return Promise.resolve() - }) - }) - .catch(err => next(err)); + res.status(200).send({ id: applicationId }); + }).catch(err => next(err)); }, ]; diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index 0093a217..e81840ff 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -63,16 +63,16 @@ module.exports = [ res.status(200).json(existingApplication); return Promise.resolve(); } - + return models.CopilotApplication.create(data) .then((result) => { res.status(201).json(result); return Promise.resolve(); }) .catch((err) => { - util.handleError('Error creating copilot application', err, req, next); - return next(err); - }); + util.handleError('Error creating copilot application', err, req, next); + return next(err); + }); }).catch((e) => { util.handleError('Error applying for copilot opportunity', e, req, next); }); diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index 80786aef..69aea8fe 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -10,7 +10,6 @@ const permissions = tcMiddleware.permissions; module.exports = [ permissions('copilotApplications.view'), (req, res, next) => { - const canAccessAllApplications = util.hasRoles(req, ADMIN_ROLES) || util.hasProjectManagerRole(req); const userId = req.authUser.userId; const opportunityId = _.parseInt(req.params.id); @@ -29,7 +28,7 @@ module.exports = [ const whereCondition = _.assign({ opportunityId, }, - canAccessAllApplications ? {} : { createdBy: userId }, + canAccessAllApplications ? {} : { createdBy: userId }, ); return models.CopilotApplication.findAll({ diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index ed32e2c3..02ce50c9 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -199,7 +199,7 @@ const buildCreateInvitePromises = (req, inviteEmails, inviteUserIds, invites, da }; const sendInviteEmail = (req, projectId, invite) => { - req.log.debug(`Sending invite email: ${JSON.stringify(req.body)}, ${projectId}, ${JSON.stringify(invite)}`) + req.log.debug(`Sending invite email: ${JSON.stringify(req.body)}, ${projectId}, ${JSON.stringify(invite)}`); req.log.debug(req.authUser); const emailEventType = CONNECT_NOTIFICATION_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED; const promises = [ @@ -295,13 +295,12 @@ module.exports = [ // whom we are inviting, because Member Service has a loose search logic and may return // users with handles whom we didn't search for .then((foundUsers) => { - if(invite.handles) { + if (invite.handles) { const lowerCaseHandles = invite.handles.map(handle => handle.toLowerCase()); return foundUsers.filter(foundUser => _.includes(lowerCaseHandles, foundUser.handleLower)); } - else { - return [] - } + + return []; }) .then((inviteUsers) => { const members = req.context.currentProjectMembers; @@ -414,7 +413,7 @@ module.exports = [ RESOURCES.PROJECT_MEMBER_INVITE, v.toJSON()); - req.log.debug(`V: ${JSON.stringify(v)}`) + req.log.debug(`V: ${JSON.stringify(v)}`); // send email invite (async) if (v.email && !v.userId && v.status === INVITE_STATUS.PENDING) { sendInviteEmail(req, projectId, v); @@ -443,7 +442,7 @@ module.exports = [ } }); }).catch((err) => { - console.log(err) + console.log(err); if (failed.length) { res.status(403).json(_.assign({}, { success: [] }, { failed })); } else next(err); From 2fd1652571aca1f3dd7e97c1f039a2f4ca89185d Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 13 May 2025 23:22:52 +0200 Subject: [PATCH 9/9] fix: check if the user is already a copilot --- src/routes/copilotOpportunity/assign.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index be588ce3..81de6e83 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -67,8 +67,8 @@ module.exports = [ const activeMembers = await models.ProjectMember.getActiveProjectMembers(projectId); const existingUser = activeMembers.find(item => item.userId === userId); - if (existingUser) { - const err = new Error(`User is already part of the project as ${existingUser.role}`); + if (existingUser && existingUser.role === 'copilot') { + const err = new Error(`User is already a copilot of this project`); err.status = 400; throw err; }