From 61633948df63373338d7e0eb76ac6ddabe7fdf6c Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 14 Apr 2025 22:48:50 +0200 Subject: [PATCH 01/22] feat: apply for copilot opportunity --- ...0250411182312-copilot_opportunity_apply.js | 56 ++++++++++++++++++ src/models/copilotApplication.js | 30 ++++++++++ src/models/copilotOpportunity.js | 1 + src/permissions/constants.js | 12 ++++ src/routes/copilotOpportunityApply/create.js | 57 +++++++++++++++++++ src/routes/index.js | 4 ++ 6 files changed, 160 insertions(+) create mode 100644 migrations/umzug/migrations/20250411182312-copilot_opportunity_apply.js create mode 100644 src/models/copilotApplication.js create mode 100644 src/routes/copilotOpportunityApply/create.js diff --git a/migrations/umzug/migrations/20250411182312-copilot_opportunity_apply.js b/migrations/umzug/migrations/20250411182312-copilot_opportunity_apply.js new file mode 100644 index 00000000..f07f8ede --- /dev/null +++ b/migrations/umzug/migrations/20250411182312-copilot_opportunity_apply.js @@ -0,0 +1,56 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('copilot_applications', { + id: { + type: Sequelize.BIGINT, + allowNull: false, + primaryKey: true, + autoIncrement: true, + }, + userId: { + type: Sequelize.BIGINT, + allowNull: false, + }, + opportunityId: { + type: Sequelize.BIGINT, + allowNull: false, + references: { + model: 'copilot_opportunities', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL', + }, + deletedAt: { + type: Sequelize.DATE, + allowNull: true, + }, + createdAt: { + type: Sequelize.DATE, + allowNull: true, + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: true, + }, + deletedBy: { + type: Sequelize.BIGINT, + allowNull: true, + }, + createdBy: { + type: Sequelize.BIGINT, + allowNull: false, + }, + updatedBy: { + type: Sequelize.BIGINT, + allowNull: false, + }, + }); + }, + + down: async (queryInterface) => { + await queryInterface.dropTable('copilot_applications'); + } +}; diff --git a/src/models/copilotApplication.js b/src/models/copilotApplication.js new file mode 100644 index 00000000..a17c14e5 --- /dev/null +++ b/src/models/copilotApplication.js @@ -0,0 +1,30 @@ +import _ from 'lodash'; +import { COPILOT_OPPORTUNITY_STATUS, COPILOT_OPPORTUNITY_TYPE } from '../constants'; + +module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { + const CopilotApplication = sequelize.define('CopilotApplication', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + opportunityId: { type: DataTypes.BIGINT, allowNull: false }, + userId: { type: DataTypes.BIGINT, allowNull: false }, + deletedAt: { type: DataTypes.DATE, allowNull: true }, + createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + deletedBy: { type: DataTypes.INTEGER, allowNull: true }, + createdBy: { type: DataTypes.INTEGER, allowNull: false }, + updatedBy: { type: DataTypes.INTEGER, allowNull: false }, + }, { + tableName: 'copilot_opportunities', + paranoid: true, + timestamps: true, + updatedAt: 'updatedAt', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + indexes: [], + }); + + CopilotApplication.associate = (models) => { + CopilotApplication.belongsTo(models.CopilotOpportunity, { as: 'copilotOpportunity', foreignKey: 'copilotOpportunityId' }); + }; + + return CopilotApplication; +}; diff --git a/src/models/copilotOpportunity.js b/src/models/copilotOpportunity.js index 7ce395c3..acb8f152 100644 --- a/src/models/copilotOpportunity.js +++ b/src/models/copilotOpportunity.js @@ -38,6 +38,7 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { CopilotOpportunity.associate = (models) => { CopilotOpportunity.belongsTo(models.CopilotRequest, { as: 'copilotRequest', foreignKey: 'copilotRequestId' }); CopilotOpportunity.belongsTo(models.Project, { as: 'project', foreignKey: 'projectId' }); + CopilotOpportunity.belongsTo(models.CopilotApplication, { as: 'copilotApplication', foreignKey: 'copilotOpportunityId' }); }; return CopilotOpportunity; diff --git a/src/permissions/constants.js b/src/permissions/constants.js index 17585d30..a7f647cd 100644 --- a/src/permissions/constants.js +++ b/src/permissions/constants.js @@ -265,6 +265,18 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export scopes: SCOPES_PROJECTS_WRITE, }, + APPLY_COPILOT_OPPORTUNITY: { + meta: { + title: 'Apply copilot opportunity', + group: 'Apply Copilot', + description: 'Who can apply for copilot opportunity.', + }, + topcoderRoles: [ + USER_ROLE.COPILOT, + ], + scopes: SCOPES_PROJECTS_WRITE, + }, + MANAGE_PROJECT_BILLING_ACCOUNT_ID: { meta: { title: 'Manage Project property "billingAccountId"', diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js new file mode 100644 index 00000000..f961b4c0 --- /dev/null +++ b/src/routes/copilotOpportunityApply/create.js @@ -0,0 +1,57 @@ +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; + +import models from '../../models'; +import util from '../../util'; +import { PERMISSION } from '../../permissions/constants'; + +const addCopilotApplicationValidations = { + body: Joi.object().keys({ + data: Joi.object() + .keys({ + opportunityId: Joi.number().required(), + }) + .required(), + }), +}; + +module.exports = [ + validate(addCopilotApplicationValidations), + async (req, res, next) => { + const { data } = req.body; + if (!util.hasPermissionByReq(PERMISSION.APPLY_COPILOT_OPPORTUNITY, req)) { + const err = new Error('Unable to apply for copilot opportunity'); + _.assign(err, { + details: JSON.stringify({ message: 'You do not have permission to apply for copilot opportunity' }), + status: 403, + }); + return next(err); + } + // default values + _.assign(data, { + userId: req.authUser.userId, + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + }); + + return models.CopilotOpportunity.findOne({ + where: { + id: data.opportunityId, + }, + }).then((opportunity) => { + if (!opportunity) { + const err = new Error('No opportunity found'); + err.status = 404; + return next(err); + } + + return models.CopilotApplication.create(data).catch((err) => { + util.handleError('Error creating copilot application', err, req, next); + return next(err); + }); + }).catch((e) => { + util.handleError('Error finding the copilot opportunity', err, req, next); + }); + }, +]; diff --git a/src/routes/index.js b/src/routes/index.js index b0022ad8..66b753f6 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -404,6 +404,10 @@ router.route('/v5/projects/copilots/opportunities') router.route('/v5/projects/copilot/opportunity/:id(\\d+)') .get(require('./copilotOpportunity/get')); +// Project copilot opportunity apply +router.route('/v5/projects/copilots/opportunity/:id(\\d+)/apply') + .post(require('./copilotOpportunityApply/create')); + // Project Estimation Items router.route('/v5/projects/:projectId(\\d+)/estimations/:estimationId(\\d+)/items') .get(require('./projectEstimationItems/list')); From 0372f83d2f3e02643cf414e0e99a9f5ed71b3395 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 14 Apr 2025 22:51:08 +0200 Subject: [PATCH 02/22] deploy pr branch to develo[ --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1acd4a4c..3dd6a457 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-577'] - deployProd: context : org-global filters: From cffc8c0c6a58a32c8f844d38b850f7cca24c944a Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Apr 2025 20:51:27 +0200 Subject: [PATCH 03/22] updated swagger doc --- docs/swagger.yaml | 38 ++++++++++++++++++-- src/routes/copilotOpportunityApply/create.js | 6 ++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index efe6e6d7..d52ba5e5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -411,7 +411,7 @@ paths: "/projects/copilots/opportunities": get: tags: - - projects copilot opportunities + - projects copilot opportunity operationId: getAllCopilotOpportunities security: - Bearer: [] @@ -444,7 +444,7 @@ paths: description: "Internal Server Error" schema: $ref: "#/definitions/ErrorModel" - "/projects/copilots/opportunities/{copilotOpportunityId}": + "/projects/copilots/opportunity/{copilotOpportunityId}": get: tags: - projects copilot opportunity @@ -471,6 +471,33 @@ paths: description: "Internal Server Error" schema: $ref: "#/definitions/ErrorModel" + "/projects/copilots/opportunity/{copilotOpportunityId}/apply": + post: + tags: + - projects copilot opportunity + operationId: applyCopilotOpportunity + security: + - Bearer: [] + description: "Retrieve a specific copilot opportunity." + parameters: + - $ref: "#/parameters/copilotOpportunityIdParam" + responses: + "200": + description: "The copilot opportunity" + schema: + $ref: "#/definitions/CopilotOpportunity" + "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: @@ -5448,6 +5475,13 @@ parameters: required: true type: integer format: int64 + copilotOpportunityIdParam: + name: copilotOpportunityId + in: path + description: copilot opportunity identifier + required: true + type: integer + format: int64 phaseIdParam: name: phaseId in: path diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index f961b4c0..9daf2e40 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -19,7 +19,8 @@ const addCopilotApplicationValidations = { module.exports = [ validate(addCopilotApplicationValidations), async (req, res, next) => { - const { data } = req.body; + const data = {}; + const copilotOpportunityId = _.parseInt(req.params.id); if (!util.hasPermissionByReq(PERMISSION.APPLY_COPILOT_OPPORTUNITY, req)) { const err = new Error('Unable to apply for copilot opportunity'); _.assign(err, { @@ -33,11 +34,12 @@ module.exports = [ userId: req.authUser.userId, createdBy: req.authUser.userId, updatedBy: req.authUser.userId, + opportunityId: copilotOpportunityId, }); return models.CopilotOpportunity.findOne({ where: { - id: data.opportunityId, + id: copilotOpportunityId, }, }).then((opportunity) => { if (!opportunity) { From bf83bcb8fb9bef5774eae710ec5d7b6a10169459 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 23 Apr 2025 23:30:51 +0200 Subject: [PATCH 04/22] fix: removed validation --- src/routes/copilotOpportunityApply/create.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index 9daf2e40..94978d9e 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -1,23 +1,10 @@ -import validate from 'express-validation'; import _ from 'lodash'; -import Joi from 'joi'; import models from '../../models'; import util from '../../util'; import { PERMISSION } from '../../permissions/constants'; -const addCopilotApplicationValidations = { - body: Joi.object().keys({ - data: Joi.object() - .keys({ - opportunityId: Joi.number().required(), - }) - .required(), - }), -}; - module.exports = [ - validate(addCopilotApplicationValidations), async (req, res, next) => { const data = {}; const copilotOpportunityId = _.parseInt(req.params.id); From dc392192349a43e750c18f6347dcc0a2728dfdc1 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Apr 2025 00:39:04 +0200 Subject: [PATCH 05/22] fix: reference error --- src/routes/copilotOpportunityApply/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index 94978d9e..f25e988c 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -40,7 +40,7 @@ module.exports = [ return next(err); }); }).catch((e) => { - util.handleError('Error finding the copilot opportunity', err, req, next); + util.handleError('Error applying for copilot opportunity', e, req, next); }); }, ]; From 22cc47435255c48505039d081014a0a6fa51dbd8 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Apr 2025 01:04:29 +0200 Subject: [PATCH 06/22] fix: model --- src/models/copilotApplication.js | 2 +- src/models/copilotOpportunity.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/copilotApplication.js b/src/models/copilotApplication.js index a17c14e5..ed72a8bc 100644 --- a/src/models/copilotApplication.js +++ b/src/models/copilotApplication.js @@ -23,7 +23,7 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { }); CopilotApplication.associate = (models) => { - CopilotApplication.belongsTo(models.CopilotOpportunity, { as: 'copilotOpportunity', foreignKey: 'copilotOpportunityId' }); + CopilotApplication.belongsTo(models.CopilotOpportunity, { as: 'copilotOpportunity', foreignKey: 'opportunityId' }); }; return CopilotApplication; diff --git a/src/models/copilotOpportunity.js b/src/models/copilotOpportunity.js index acb8f152..cf2c8f8d 100644 --- a/src/models/copilotOpportunity.js +++ b/src/models/copilotOpportunity.js @@ -38,7 +38,7 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { CopilotOpportunity.associate = (models) => { CopilotOpportunity.belongsTo(models.CopilotRequest, { as: 'copilotRequest', foreignKey: 'copilotRequestId' }); CopilotOpportunity.belongsTo(models.Project, { as: 'project', foreignKey: 'projectId' }); - CopilotOpportunity.belongsTo(models.CopilotApplication, { as: 'copilotApplication', foreignKey: 'copilotOpportunityId' }); + CopilotOpportunity.hasMany(models.CopilotApplication, { as: 'copilotApplication', foreignKey: 'opportunityId' }); }; return CopilotOpportunity; From 472acdfeff5e9bf48ea9695e7b2b3491677ed674 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Apr 2025 14:30:05 +0200 Subject: [PATCH 07/22] fix: model --- src/models/copilotApplication.js | 2 +- src/models/copilotOpportunity.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/copilotApplication.js b/src/models/copilotApplication.js index ed72a8bc..719c3a0d 100644 --- a/src/models/copilotApplication.js +++ b/src/models/copilotApplication.js @@ -23,7 +23,7 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { }); CopilotApplication.associate = (models) => { - CopilotApplication.belongsTo(models.CopilotOpportunity, { as: 'copilotOpportunity', foreignKey: 'opportunityId' }); + CopilotApplication.hasMany(models.CopilotOpportunity, { as: 'copilotOpportunity', foreignKey: 'opportunityId' }); }; return CopilotApplication; diff --git a/src/models/copilotOpportunity.js b/src/models/copilotOpportunity.js index cf2c8f8d..df3d7bf6 100644 --- a/src/models/copilotOpportunity.js +++ b/src/models/copilotOpportunity.js @@ -38,7 +38,7 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { CopilotOpportunity.associate = (models) => { CopilotOpportunity.belongsTo(models.CopilotRequest, { as: 'copilotRequest', foreignKey: 'copilotRequestId' }); CopilotOpportunity.belongsTo(models.Project, { as: 'project', foreignKey: 'projectId' }); - CopilotOpportunity.hasMany(models.CopilotApplication, { as: 'copilotApplication', foreignKey: 'opportunityId' }); + CopilotOpportunity.belongsTo(models.CopilotApplication, { as: 'copilotApplication', foreignKey: 'opportunityId' }); }; return CopilotOpportunity; From 5a0b44fd46183afbeaf0d0063b152454c7dff1e5 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Apr 2025 15:17:37 +0200 Subject: [PATCH 08/22] fix: model --- src/models/copilotOpportunity.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/models/copilotOpportunity.js b/src/models/copilotOpportunity.js index df3d7bf6..7ce395c3 100644 --- a/src/models/copilotOpportunity.js +++ b/src/models/copilotOpportunity.js @@ -38,7 +38,6 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { CopilotOpportunity.associate = (models) => { CopilotOpportunity.belongsTo(models.CopilotRequest, { as: 'copilotRequest', foreignKey: 'copilotRequestId' }); CopilotOpportunity.belongsTo(models.Project, { as: 'project', foreignKey: 'projectId' }); - CopilotOpportunity.belongsTo(models.CopilotApplication, { as: 'copilotApplication', foreignKey: 'opportunityId' }); }; return CopilotOpportunity; From 7c637115e0cd4987107f25cdaca7efc98d6bfd5a Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Apr 2025 15:59:58 +0200 Subject: [PATCH 09/22] fix: model --- src/models/copilotApplication.js | 4 ---- src/models/copilotOpportunity.js | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/models/copilotApplication.js b/src/models/copilotApplication.js index 719c3a0d..d71c95cf 100644 --- a/src/models/copilotApplication.js +++ b/src/models/copilotApplication.js @@ -22,9 +22,5 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { indexes: [], }); - CopilotApplication.associate = (models) => { - CopilotApplication.hasMany(models.CopilotOpportunity, { as: 'copilotOpportunity', foreignKey: 'opportunityId' }); - }; - return CopilotApplication; }; diff --git a/src/models/copilotOpportunity.js b/src/models/copilotOpportunity.js index 7ce395c3..81355750 100644 --- a/src/models/copilotOpportunity.js +++ b/src/models/copilotOpportunity.js @@ -38,6 +38,7 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { CopilotOpportunity.associate = (models) => { CopilotOpportunity.belongsTo(models.CopilotRequest, { as: 'copilotRequest', foreignKey: 'copilotRequestId' }); CopilotOpportunity.belongsTo(models.Project, { as: 'project', foreignKey: 'projectId' }); + CopilotOpportunity.hasMany(models.CopilotApplication, { as: 'copilotApplications', foreignKey: 'copilotOpportunityId' }); }; return CopilotOpportunity; From f0d083f27835af48a470aa2ac265ebcc62da2b6e Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Apr 2025 16:04:42 +0200 Subject: [PATCH 10/22] fix: model --- src/models/copilotApplication.js | 4 ++++ src/models/copilotOpportunity.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/models/copilotApplication.js b/src/models/copilotApplication.js index d71c95cf..ed72a8bc 100644 --- a/src/models/copilotApplication.js +++ b/src/models/copilotApplication.js @@ -22,5 +22,9 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { indexes: [], }); + CopilotApplication.associate = (models) => { + CopilotApplication.belongsTo(models.CopilotOpportunity, { as: 'copilotOpportunity', foreignKey: 'opportunityId' }); + }; + return CopilotApplication; }; diff --git a/src/models/copilotOpportunity.js b/src/models/copilotOpportunity.js index 81355750..446cab81 100644 --- a/src/models/copilotOpportunity.js +++ b/src/models/copilotOpportunity.js @@ -38,7 +38,7 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { CopilotOpportunity.associate = (models) => { CopilotOpportunity.belongsTo(models.CopilotRequest, { as: 'copilotRequest', foreignKey: 'copilotRequestId' }); CopilotOpportunity.belongsTo(models.Project, { as: 'project', foreignKey: 'projectId' }); - CopilotOpportunity.hasMany(models.CopilotApplication, { as: 'copilotApplications', foreignKey: 'copilotOpportunityId' }); + CopilotOpportunity.hasMany(models.CopilotApplication, { as: 'copilotApplications', foreignKey: 'opportunityId' }); }; return CopilotOpportunity; From aa606699fe723c68647ea278f28bf5de37c376c9 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Apr 2025 16:41:03 +0200 Subject: [PATCH 11/22] fix: model --- new | 12 ++++++++++++ src/models/copilotApplication.js | 13 +++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 new diff --git a/new b/new new file mode 100644 index 00000000..586be550 --- /dev/null +++ b/new @@ -0,0 +1,12 @@ +## +# Host Database +# +# localhost is used to configure the loopback interface +# when the system is booting. Do not change this entry. +## +127.0.0.1 dockerhost +127.0.0.1 localhost +255.255.255.255 broadcasthost +::1 localhost +127.0.0.1 local.topcoder-dev.com +127.0.0.1 local.topcoder.com diff --git a/src/models/copilotApplication.js b/src/models/copilotApplication.js index ed72a8bc..d50e22d0 100644 --- a/src/models/copilotApplication.js +++ b/src/models/copilotApplication.js @@ -4,7 +4,16 @@ import { COPILOT_OPPORTUNITY_STATUS, COPILOT_OPPORTUNITY_TYPE } from '../constan module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { const CopilotApplication = sequelize.define('CopilotApplication', { id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, - opportunityId: { type: DataTypes.BIGINT, allowNull: false }, + opportunityId: { + type: DataTypes.BIGINT, + allowNull: false, + references: { + model: 'copilot_opportunities', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE' + }, userId: { type: DataTypes.BIGINT, allowNull: false }, deletedAt: { type: DataTypes.DATE, allowNull: true }, createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, @@ -13,7 +22,7 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { createdBy: { type: DataTypes.INTEGER, allowNull: false }, updatedBy: { type: DataTypes.INTEGER, allowNull: false }, }, { - tableName: 'copilot_opportunities', + tableName: 'copilot_applications', paranoid: true, timestamps: true, updatedAt: 'updatedAt', From 47664c866f7d5daf84ec4e7ff7a11acb28b8110d Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Apr 2025 17:17:35 +0200 Subject: [PATCH 12/22] fix: model --- src/routes/copilotOpportunityApply/create.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index f25e988c..79d4e673 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -35,7 +35,12 @@ module.exports = [ return next(err); } - return models.CopilotApplication.create(data).catch((err) => { + 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); }); From 1978a22971e7004015b9e5bcbd158ec66026dcac Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Apr 2025 17:21:54 +0200 Subject: [PATCH 13/22] fix: validations --- src/routes/copilotOpportunityApply/create.js | 22 +++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index 79d4e673..016a25fb 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -3,6 +3,7 @@ import _ from 'lodash'; import models from '../../models'; import util from '../../util'; import { PERMISSION } from '../../permissions/constants'; +import { COPILOT_OPPORTUNITY_STATUS } from '../../constants'; module.exports = [ async (req, res, next) => { @@ -28,12 +29,31 @@ module.exports = [ where: { id: copilotOpportunityId, }, - }).then((opportunity) => { + }).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 existingApplication = await models.CopilotApplication.findOne({ + where: { + opportunityId: opportunity.id, + userId: req.authUser.userId, + }, + }); + + if (existingApplication) { + const err = new Error('User already applied for this opportunity'); + err.status = 400; + return next(err); + } return models.CopilotApplication.create(data) .then((result) => { From 3cac3253416c46434ef2fee9048b6cede122ba4d Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 24 Apr 2025 17:42:06 +0200 Subject: [PATCH 14/22] removed unnecessary file --- new | 12 ------------ src/models/copilotApplication.js | 1 - 2 files changed, 13 deletions(-) delete mode 100644 new diff --git a/new b/new deleted file mode 100644 index 586be550..00000000 --- a/new +++ /dev/null @@ -1,12 +0,0 @@ -## -# Host Database -# -# localhost is used to configure the loopback interface -# when the system is booting. Do not change this entry. -## -127.0.0.1 dockerhost -127.0.0.1 localhost -255.255.255.255 broadcasthost -::1 localhost -127.0.0.1 local.topcoder-dev.com -127.0.0.1 local.topcoder.com diff --git a/src/models/copilotApplication.js b/src/models/copilotApplication.js index d50e22d0..15366a92 100644 --- a/src/models/copilotApplication.js +++ b/src/models/copilotApplication.js @@ -1,5 +1,4 @@ import _ from 'lodash'; -import { COPILOT_OPPORTUNITY_STATUS, COPILOT_OPPORTUNITY_TYPE } from '../constants'; module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { const CopilotApplication = sequelize.define('CopilotApplication', { From 7ad57ecae2e8351f007291ef56efc3935f2dffc3 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Apr 2025 17:20:07 +0200 Subject: [PATCH 15/22] fix: added notes column --- .../migrations/20250411182312-copilot_opportunity_apply.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/migrations/umzug/migrations/20250411182312-copilot_opportunity_apply.js b/migrations/umzug/migrations/20250411182312-copilot_opportunity_apply.js index f07f8ede..7d29919e 100644 --- a/migrations/umzug/migrations/20250411182312-copilot_opportunity_apply.js +++ b/migrations/umzug/migrations/20250411182312-copilot_opportunity_apply.js @@ -23,6 +23,10 @@ module.exports = { onUpdate: 'CASCADE', onDelete: 'SET NULL', }, + notes: { + type: Sequelize.TEXT, + allowNull: true, + }, deletedAt: { type: Sequelize.DATE, allowNull: true, From 8dfcdf0333e517df3f37f93f35864104b4455235 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Apr 2025 17:23:34 +0200 Subject: [PATCH 16/22] fix: added notes from request --- src/routes/copilotOpportunityApply/create.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index 016a25fb..3e96bb9d 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -1,13 +1,24 @@ import _ from 'lodash'; +import validate from 'express-validation'; import models from '../../models'; import util from '../../util'; import { PERMISSION } from '../../permissions/constants'; import { COPILOT_OPPORTUNITY_STATUS } from '../../constants'; +const applyCopilotRequestValidations = { + body: Joi.object().keys({ + data: Joi.object() + .keys({ + notes: Joi.string(), + }), + }), +}; + module.exports = [ + validate(applyCopilotRequestValidations), async (req, res, next) => { - const data = {}; + const data = req.body; const copilotOpportunityId = _.parseInt(req.params.id); if (!util.hasPermissionByReq(PERMISSION.APPLY_COPILOT_OPPORTUNITY, req)) { const err = new Error('Unable to apply for copilot opportunity'); From 4a13b50867fce1062a08b967494d93c4992e915f Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Apr 2025 17:29:03 +0200 Subject: [PATCH 17/22] updated swagger file --- docs/swagger.yaml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d52ba5e5..e38fbe25 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -11,10 +11,10 @@ info: You can also set a custom page size up to 100 with the `perPage` parameter. Pagination response data is included in http headers. By Default, the response header contains links with `next`, `last`, `first`, `prev` resource links. -host: "localhost:3000" +host: "api.topcoder-dev.com" basePath: /v5 schemes: - - http + - https produces: - application/json consumes: @@ -481,6 +481,10 @@ paths: description: "Retrieve a specific copilot opportunity." parameters: - $ref: "#/parameters/copilotOpportunityIdParam" + - in: body + name: body + schema: + $ref: "#/definitions/ApplyCopilotOpportunity" responses: "200": description: "The copilot opportunity" @@ -6218,6 +6222,13 @@ definitions: - customer - manager - copilot + ApplyCopilotOpportunity: + title: Apply copilot CopilotOpportunity + type: object + properties: + notes: + type: string + description: notes about applying copilot opportunity NewProjectAttachment: title: Project attachment request type: object From 350d155902431fab16036a387f2ee13c13323594 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 28 Apr 2025 17:41:02 +0200 Subject: [PATCH 18/22] debug logs --- src/routes/copilotOpportunityApply/create.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index 3e96bb9d..0df91020 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -1,5 +1,6 @@ import _ from 'lodash'; import validate from 'express-validation'; +import Joi from 'joi'; import models from '../../models'; import util from '../../util'; @@ -19,6 +20,7 @@ module.exports = [ validate(applyCopilotRequestValidations), async (req, res, next) => { const data = req.body; + console.log(data, 'debug data'); const copilotOpportunityId = _.parseInt(req.params.id); if (!util.hasPermissionByReq(PERMISSION.APPLY_COPILOT_OPPORTUNITY, req)) { const err = new Error('Unable to apply for copilot opportunity'); From c922d0c7daf96e3384b357b9cc762068f77a7b4f Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Apr 2025 19:17:49 +0200 Subject: [PATCH 19/22] added notes to model --- src/models/copilotApplication.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/models/copilotApplication.js b/src/models/copilotApplication.js index 15366a92..c472da60 100644 --- a/src/models/copilotApplication.js +++ b/src/models/copilotApplication.js @@ -13,6 +13,10 @@ module.exports = function defineCopilotOpportunity(sequelize, DataTypes) { onUpdate: 'CASCADE', onDelete: 'CASCADE' }, + notes: { + type: DataTypes.TEXT, + allowNull: true + }, userId: { type: DataTypes.BIGINT, allowNull: false }, deletedAt: { type: DataTypes.DATE, allowNull: true }, createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, From c3e8fca971a14c2fb7247794298442d8ae5b547c Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Apr 2025 20:22:06 +0200 Subject: [PATCH 20/22] sanitize notes --- src/routes/copilotOpportunityApply/create.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index 0df91020..157649e0 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -9,18 +9,14 @@ import { COPILOT_OPPORTUNITY_STATUS } from '../../constants'; const applyCopilotRequestValidations = { body: Joi.object().keys({ - data: Joi.object() - .keys({ - notes: Joi.string(), - }), + notes: Joi.string(), }), }; module.exports = [ validate(applyCopilotRequestValidations), async (req, res, next) => { - const data = req.body; - console.log(data, 'debug data'); + const { notes } = req.body; const copilotOpportunityId = _.parseInt(req.params.id); if (!util.hasPermissionByReq(PERMISSION.APPLY_COPILOT_OPPORTUNITY, req)) { const err = new Error('Unable to apply for copilot opportunity'); @@ -36,8 +32,11 @@ module.exports = [ createdBy: req.authUser.userId, updatedBy: req.authUser.userId, opportunityId: copilotOpportunityId, + notes: notes ? req.sanitize(notes) : null, }); + console.log(data, 'debug data data'); + return models.CopilotOpportunity.findOne({ where: { id: copilotOpportunityId, From 5329b81dae72bac4cf511e0989064ecba9f52dd8 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Apr 2025 20:41:39 +0200 Subject: [PATCH 21/22] sanitize notes --- src/routes/copilotOpportunityApply/create.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index 157649e0..51365269 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -26,14 +26,14 @@ module.exports = [ }); return next(err); } - // default values - _.assign(data, { + + const data = { userId: req.authUser.userId, createdBy: req.authUser.userId, updatedBy: req.authUser.userId, opportunityId: copilotOpportunityId, notes: notes ? req.sanitize(notes) : null, - }); + }; console.log(data, 'debug data data'); From ca59b3302b6016d6e220a66217f4a90cbec67235 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Apr 2025 21:42:53 +0200 Subject: [PATCH 22/22] removed console log --- src/routes/copilotOpportunityApply/create.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/routes/copilotOpportunityApply/create.js b/src/routes/copilotOpportunityApply/create.js index 51365269..dcf2cbe5 100644 --- a/src/routes/copilotOpportunityApply/create.js +++ b/src/routes/copilotOpportunityApply/create.js @@ -35,8 +35,6 @@ module.exports = [ notes: notes ? req.sanitize(notes) : null, }; - console.log(data, 'debug data data'); - return models.CopilotOpportunity.findOne({ where: { id: copilotOpportunityId,