diff --git a/.circleci/config.yml b/.circleci/config.yml index d634f7e6..e22ef291 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,7 +11,7 @@ install_dependency: &install_dependency install_deploysuite: &install_deploysuite name: Installation of install_deploysuite. command: | - git clone --branch v1.4.2 https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript + git clone --branch master https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript cp ./../buildscript/master_deploy.sh . cp ./../buildscript/buildenv.sh . cp ./../buildscript/awsconfiguration.sh . diff --git a/data/demo-data.json b/data/demo-data.json index ce9f2456..59b2ba50 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -7621,10 +7621,30 @@ } ], "Role": [ + { + "id": "f5e01b7c-466f-45c8-989c-16ff831d7e59", + "name": "Custom", + "description": null, + "listOfSkills": null, + "rates": [{ + "global": 1200, + "offShore": 1200, + "inCountry": 1200 + }], + "numberOfMembers": null, + "numberOfMembersAvailable": null, + "imageUrl": null, + "timeToCandidate": null, + "timeToInterview": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-27T21:43:08.201Z", + "updatedAt": "2021-05-27T21:43:08.201Z" + }, { "id": "c145247d-5757-463d-9317-ff9e7026d403", "name": "Angular Developer", - "description": "Angular is an open-source, client-side framework based on TypeScript and designed for building web applications.", + "description": "* Writes tested and documented JavaScript, HTML and CSS\n* Makes design and technical decisions for AngularJS projects\n* Develops application code and unit test in the AngularJS, Rest Web Services and Java technologies", "listOfSkills": [ "database", "winforms", @@ -7656,7 +7676,7 @@ { "id": "d7ff0289-d3ea-44d8-b39a-53bba5b5b309", "name": "Dev Ops Engineer", - "description": "A DevOps engineer introduces processes, tools, and methodologies to balance needs throughout the software development life cycle, from coding and deployment, to maintenance and updates.", + "description": "* Introduces processes, tools, and methodologies\n* Balances needs throughout the software development life cycle\n* Configures server images, optimizes task performance in correspondence with engineers", "listOfSkills": [ "dropwizard", "nginx", @@ -7700,7 +7720,7 @@ { "id": "e7b7e818-40d4-4102-b486-09bdd21400b8", "name": "Salesforce Developer", - "description": "A Salesforce developer is a programmer who builds Salesforce applications across various PaaS (Platform as a Service) platforms.", + "description": "* Meets with project managers to determine CRM needs\n* Develops customized solutions within the Salesforce platform\n* Designs, codes, and implements Salesforce applications\n* Creates timelines and development goals\n* Tests the stability and functionality of the application\n* Troubleshoots and fixes bugs\n* Writes documents and provides technical training for Salesforce Staff\n* Maintains the security and integrity of the application software", "listOfSkills": [ "docker", ".net", diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 39fabfb8..845523ab 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "3a5bab78-49d6-4dca-9aea-e8688564ac98", + "_postman_id": "6f274c86-24a5-412e-95e6-fafa34e2a936", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -8182,10 +8182,35 @@ "value": "id,workPeriods.id,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.payments.id,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.payments.id,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.payments", + "disabled": true } ] } @@ -8351,10 +8376,35 @@ "value": "id,status,workPeriods.id,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods.id,workPeriods.payments.id,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods.id,workPeriods.payments.id,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,status,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods.payments", + "disabled": true } ] } @@ -8525,10 +8575,35 @@ "value": "id,rateType,workPeriods.id,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods.id,workPeriods.payments.id,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods.id,workPeriods.payments.id,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,rateType,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods.payments", + "disabled": true } ] } @@ -8699,10 +8774,35 @@ "value": "id,startDate,workPeriods.id,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,startDate,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods.payments", + "disabled": true } ] } @@ -8873,10 +8973,35 @@ "value": "id,endDate,workPeriods.id,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,endDate,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods.payments", + "disabled": true } ] } @@ -9047,10 +9172,35 @@ "value": "id,customerRate,workPeriods.id,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,customerRate,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods.payments", + "disabled": true } ] } @@ -9221,10 +9371,35 @@ "value": "id,memberRate,workPeriods.id,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,memberRate,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods.payments", + "disabled": true } ] } @@ -9388,10 +9563,35 @@ "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.startDate,workPeriods.payments", + "disabled": true } ] } @@ -9555,10 +9755,35 @@ "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.startDate,workPeriods.daysWorked,workPeriods.payments", + "disabled": true } ] } @@ -9722,10 +9947,35 @@ "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.startDate,workPeriods.daysPaid,workPeriods.payments", + "disabled": true } ] } @@ -9889,10 +10139,35 @@ "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.startDate,workPeriods.paymentTotal,workPeriods.payments", + "disabled": true } ] } @@ -10056,10 +10331,35 @@ "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.startDate,workPeriods.paymentStatus,workPeriods.payments", + "disabled": true } ] } @@ -11092,7 +11392,7 @@ "pm.test('Status code is 403', function () {\r", " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You don't have access to view memberRate and paymentTotal\")\r", + " pm.expect(response.message).to.eq(\"You don't have access to view memberRate, paymentTotal and payments\")\r", "});" ], "type": "text/javascript" @@ -11137,7 +11437,7 @@ "pm.test('Status code is 403', function () {\r", " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You don't have access to view memberRate and paymentTotal\")\r", + " pm.expect(response.message).to.eq(\"You don't have access to view memberRate, paymentTotal and payments\")\r", "});" ], "type": "text/javascript" @@ -11967,7 +12267,7 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"Can not filter or sort by some field which is not included in fields\")\r", + " pm.expect(response.message).to.eq(\"Can not filter or sort by ResourceBooking field which is not included in fields\")\r", "});" ], "type": "text/javascript" @@ -12015,7 +12315,7 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"Can not filter or sort by some field which is not included in fields\")\r", + " pm.expect(response.message).to.eq(\"Can not filter or sort by ResourceBooking field which is not included in fields\")\r", "});" ], "type": "text/javascript" @@ -12155,7 +12455,7 @@ "pm.test('Status code is 403', function () {\r", " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You don't have access to view memberRate and paymentTotal\")\r", + " pm.expect(response.message).to.eq(\"You don't have access to view memberRate, paymentTotal and payments\")\r", "});" ], "type": "text/javascript" @@ -17353,7 +17653,7 @@ "response": [] }, { - "name": "create role Niche", + "name": "create role Custom", "event": [ { "listen": "test", @@ -17378,7 +17678,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"name\": \"Niche\",\n \"rates\": [\n {\n \"global\": 10,\n \"inCountry\": 10,\n \"offShore\": 10\n }\n ]\n}", + "raw": "{\n \"name\": \"Custom\",\n \"rates\": [\n {\n \"global\": 1200,\n \"inCountry\": 1200,\n \"offShore\": 1200\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -18150,7 +18450,7 @@ "pm.test('Status code is 200', function () {\r", " pm.response.to.have.status(200);\r", " const response = pm.response.json()\r", - " pm.expect(response.name).to.eq(\"Niche\")\r", + " pm.expect(response.name).to.eq(\"Custom\")\r", "});" ], "type": "text/javascript" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a8abc4e4..6ffdf86d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1400,7 +1400,7 @@ paths: parameters: - in: query name: fields - description: the field names to be returned from both ResourceBooking and WorkPeriod + description: the field names to be returned from all ResourceBooking, WorkPeriod and WorkPeriodPayment required: false schema: type: string @@ -1537,6 +1537,21 @@ paths: schema: type: string description: The user handle. + - in: query + name: workPeriods.payments.status + required: false + schema: + type: string + description: The status of the payment + - in: query + name: workPeriods.payments.days + required: false + schema: + type: integer + minimum: 1 + maximum: 5 + example: 3 + description: The workdays to pay responses: "200": @@ -1627,7 +1642,7 @@ paths: type: boolean - in: query name: fields - description: the field names to be returned from both ResourceBooking and WorkPeriod + description: the field names to be returned from all ResourceBooking, WorkPeriod and WorkPeriodPayment required: false schema: type: string @@ -5261,6 +5276,11 @@ components: isExternalMember: type: boolean description: "Is the user external member" + skillsMatch: + type: number + format: float + description: "Rate at which searched skills match the given role" + example: 0.75 SubmitTeamRequestBody: properties: teamName: diff --git a/migrations/2021-06-18-make-payment-scheduler-step-optional.js b/migrations/2021-06-18-make-payment-scheduler-step-optional.js new file mode 100644 index 00000000..ddcecef1 --- /dev/null +++ b/migrations/2021-06-18-make-payment-scheduler-step-optional.js @@ -0,0 +1,15 @@ +const config = require('config') +const _ = require('lodash') +const { PaymentSchedulerStatus } = require('../app-constants') +/* + * Make Job payment_schedulers step optional. + */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.sequelize.query(`ALTER TABLE ${config.DB_SCHEMA_NAME}.payment_schedulers ALTER COLUMN step DROP NOT NULL`) + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.sequelize.query(`ALTER TABLE ${config.DB_SCHEMA_NAME}.payment_schedulers ALTER COLUMN step SET NOT NULL`) + } +} \ No newline at end of file diff --git a/scripts/demo-payment-scheduler/data.json b/scripts/demo-payment-scheduler/data.json index 5023842c..8eafffec 100644 --- a/scripts/demo-payment-scheduler/data.json +++ b/scripts/demo-payment-scheduler/data.json @@ -43,6 +43,8 @@ "startDate":"2020-09-27", "endDate":"2020-10-03", "daysWorked":4, + "daysPaid": 3, + "paymentTotal": 0, "memberRate":27.06, "customerRate":13.13, "paymentStatus":"partially-completed", @@ -59,6 +61,8 @@ "startDate":"2020-10-18", "endDate":"2020-10-24", "daysWorked":4, + "daysPaid": 3, + "paymentTotal": 0, "memberRate":4.08, "customerRate":3.89, "paymentStatus":"cancelled", @@ -75,6 +79,8 @@ "startDate":"2020-10-25", "endDate":"2020-10-31", "daysWorked":3, + "daysPaid": 3, + "paymentTotal": 0, "memberRate":15.61, "customerRate":9.76, "paymentStatus":"pending", @@ -91,6 +97,8 @@ "startDate":"2020-10-11", "endDate":"2020-10-17", "daysWorked":4, + "daysPaid": 3, + "paymentTotal": 0, "memberRate":10.82, "customerRate":30.71, "paymentStatus":"pending", diff --git a/scripts/demo-payment-scheduler/index.js b/scripts/demo-payment-scheduler/index.js index 8f76a814..5cb0a0b5 100644 --- a/scripts/demo-payment-scheduler/index.js +++ b/scripts/demo-payment-scheduler/index.js @@ -13,6 +13,8 @@ for (let i = 0; i < 1000; i++) { amount: _.round(_.random(1000, true), 2), status: 'scheduled', billingAccountId: data.ResourceBooking.billingAccountId, + memberRate: data.ResourceBooking.memberRate, + days: 4, createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', updatedBy: null, createdAt: `2021-05-19T21:3${i % 10}:46.507Z`, diff --git a/src/controllers/RoleController.js b/src/controllers/RoleController.js index 747cbe4d..084a095a 100644 --- a/src/controllers/RoleController.js +++ b/src/controllers/RoleController.js @@ -10,7 +10,7 @@ const service = require('../services/RoleService') * @param res the response */ async function getRole (req, res) { - res.send(await service.getRole(req.authUser, req.params.id, req.query.fromDb)) + res.send(await service.getRole(req.params.id, req.query.fromDb)) } /** @@ -47,7 +47,7 @@ async function deleteRole (req, res) { * @param res the response */ async function searchRoles (req, res) { - res.send(await service.searchRoles(req.authUser, req.query)) + res.send(await service.searchRoles(req.query)) } module.exports = { diff --git a/src/models/PaymentScheduler.js b/src/models/PaymentScheduler.js index f8f64dfa..c030ef04 100644 --- a/src/models/PaymentScheduler.js +++ b/src/models/PaymentScheduler.js @@ -50,8 +50,7 @@ module.exports = (sequelize) => { allowNull: false }, step: { - type: Sequelize.ENUM(_.values(PaymentSchedulerStatus)), - allowNull: false + type: Sequelize.ENUM(_.values(PaymentSchedulerStatus)) }, status: { type: Sequelize.ENUM( diff --git a/src/models/ResourceBooking.js b/src/models/ResourceBooking.js index 54a95b89..580e6e96 100644 --- a/src/models/ResourceBooking.js +++ b/src/models/ResourceBooking.js @@ -42,13 +42,32 @@ module.exports = (sequelize) => { }] // Select WorkPeriod fields if (!options.allWorkPeriods) { - criteria.include[0].attributes = _.map(options.fieldsWP, f => _.split(f, '.')[1]) + if (options.fieldsWP && options.fieldsWP.length > 0) { + criteria.include[0].attributes = _.map(options.fieldsWP, f => _.split(f, '.')[1]) + } else { + // we should include at least one workPeriod field + // if fields criteria has no workPeriod field but have workPeriodPayment field + criteria.include[0].attributes = ['id'] + } } else if (options.excludeWP && options.excludeWP.length > 0) { criteria.include[0].attributes = { exclude: _.map(options.excludeWP, f => _.split(f, '.')[1]) } } + // Include WorkPeriodPayment Model + if (options.withWorkPeriodPayments) { + criteria.include[0].include = [{ + model: ResourceBooking._models.WorkPeriodPayment, + as: 'payments', + required: false + }] + // Select WorkPeriodPayment fields + if (!options.allWorkPeriodPayments) { + criteria.include[0].include[0].attributes = _.map(options.fieldsWPP, f => _.split(f, '.')[2]) + } else if (options.excludeWPP && options.excludeWPP.length > 0) { + criteria.include[0].include[0].attributes = { exclude: _.map(options.excludeWPP, f => _.split(f, '.')[2]) } + } + } } } - const resourceBooking = await ResourceBooking.findOne(criteria) if (!resourceBooking) { throw new errors.NotFoundError(`id: ${id} "ResourceBooking" doesn't exists.`) diff --git a/src/routes/RoleRoutes.js b/src/routes/RoleRoutes.js index 7230b593..ce8441a9 100644 --- a/src/routes/RoleRoutes.js +++ b/src/routes/RoleRoutes.js @@ -14,16 +14,12 @@ module.exports = { get: { controller: 'RoleController', method: 'searchRoles', - auth: 'jwt', - scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] } }, '/taas-roles/:id': { get: { controller: 'RoleController', method: 'getRole', - auth: 'jwt', - scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] }, patch: { controller: 'RoleController', diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index b82f4f02..941be406 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -24,8 +24,6 @@ module.exports = { get: { controller: 'TeamController', method: 'searchSkills', - auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] } }, '/taas-teams/me': { @@ -94,8 +92,6 @@ module.exports = { post: { controller: 'TeamController', method: 'roleSearchRequest', - auth: 'jwt', - scopes: [constants.Scopes.CREATE_ROLE_SEARCH_REQUEST] } }, '/taas-teams/submitTeamRequest': { diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index 1f283512..33ba9aa5 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -131,7 +131,7 @@ createJobCandidate.schema = Joi.object().keys({ userId: Joi.string().uuid().required(), externalId: Joi.string().allow(null), resume: Joi.string().uri().allow(null), - remark: Joi.string().allow(null) + remark: Joi.stringAllowEmpty().allow(null) }).required() }).required() @@ -178,7 +178,7 @@ partiallyUpdateJobCandidate.schema = Joi.object().keys({ status: Joi.jobCandidateStatus(), externalId: Joi.string().allow(null), resume: Joi.string().uri().allow(null), - remark: Joi.string().allow(null) + remark: Joi.stringAllowEmpty().allow(null) }).required() }).required() @@ -195,18 +195,22 @@ async function fullyUpdateJobCandidate (currentUser, id, data) { return updateJobCandidate(currentUser, id, data) } -fullyUpdateJobCandidate.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.string().uuid().required(), - data: Joi.object().keys({ - jobId: Joi.string().uuid().required(), - userId: Joi.string().uuid().required(), - status: Joi.jobCandidateStatus().default('open'), - externalId: Joi.string().allow(null).default(null), - resume: Joi.string().uri().allow('').allow(null).default(null), - remark: Joi.string().allow('').allow(null).default(null) - }).required() -}).required() +fullyUpdateJobCandidate.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.string().uuid().required(), + data: Joi.object() + .keys({ + jobId: Joi.string().uuid().required(), + userId: Joi.string().uuid().required(), + status: Joi.jobCandidateStatus().default("open"), + externalId: Joi.string().allow(null).default(null), + resume: Joi.string().uri().allow("").allow(null).default(null), + remark: Joi.stringAllowEmpty().allow(null) + }) + .required(), + }) + .required(); /** * Delete jobCandidate by id diff --git a/src/services/JobService.js b/src/services/JobService.js index 06e3b67c..b4a50665 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -208,9 +208,9 @@ createJob.schema = Joi.object() minSalary: Joi.number().integer().allow(null), maxSalary: Joi.number().integer().allow(null), hoursPerWeek: Joi.number().integer().allow(null), - jobLocation: Joi.string().allow(null).allow(''), - jobTimezone: Joi.string().allow(null).allow(''), - currency: Joi.string().allow(null).allow(''), + jobLocation: Joi.stringAllowEmpty().allow(null), + jobTimezone: Joi.stringAllowEmpty().allow(null), + currency: Joi.stringAllowEmpty().allow(null), roleIds: Joi.array().items(Joi.string().uuid().required()) }) .required() @@ -270,31 +270,35 @@ async function partiallyUpdateJob (currentUser, id, data) { return updateJob(currentUser, id, data, false) } -partiallyUpdateJob.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.string().guid().required(), - data: Joi.object().keys({ - status: Joi.jobStatus(), - externalId: Joi.string().allow(null), - description: Joi.stringAllowEmpty().allow(null), - title: Joi.title(), - startDate: Joi.date().allow(null), - duration: Joi.number().integer().min(1).allow(null), - numPositions: Joi.number().integer().min(1), - resourceType: Joi.stringAllowEmpty().allow(null), - rateType: Joi.rateType().allow(null), - workload: Joi.workload().allow(null), - skills: Joi.array().items(Joi.string().uuid()), - isApplicationPageActive: Joi.boolean(), - minSalary: Joi.number().integer(), - maxSalary: Joi.number().integer(), - hoursPerWeek: Joi.number().integer(), - jobLocation: Joi.string(), - jobTimezone: Joi.string(), - currency: Joi.string(), - roleIds: Joi.array().items(Joi.string().uuid().required()).allow(null) - }).required() -}).required() +partiallyUpdateJob.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.string().guid().required(), + data: Joi.object() + .keys({ + status: Joi.jobStatus(), + externalId: Joi.string().allow(null), + description: Joi.stringAllowEmpty().allow(null), + title: Joi.title(), + startDate: Joi.date().allow(null), + duration: Joi.number().integer().min(1).allow(null), + numPositions: Joi.number().integer().min(1), + resourceType: Joi.stringAllowEmpty().allow(null), + rateType: Joi.rateType().allow(null), + workload: Joi.workload().allow(null), + skills: Joi.array().items(Joi.string().uuid()), + isApplicationPageActive: Joi.boolean(), + minSalary: Joi.number().integer(), + maxSalary: Joi.number().integer(), + hoursPerWeek: Joi.number().integer(), + jobLocation: Joi.stringAllowEmpty().allow(null), + jobTimezone: Joi.stringAllowEmpty().allow(null), + currency: Joi.stringAllowEmpty().allow(null), + roleIds: Joi.array().items(Joi.string().uuid().required()).allow(null), + }) + .required(), + }) + .required(); /** * Fully update job by id @@ -327,9 +331,9 @@ fullyUpdateJob.schema = Joi.object().keys({ minSalary: Joi.number().integer().allow(null), maxSalary: Joi.number().integer().allow(null), hoursPerWeek: Joi.number().integer().allow(null), - jobLocation: Joi.string().allow(null), - jobTimezone: Joi.string().allow(null), - currency: Joi.string().allow(null), + jobLocation: Joi.stringAllowEmpty().allow(null), + jobTimezone: Joi.stringAllowEmpty().allow(null), + currency: Joi.stringAllowEmpty().allow(null), roleIds: Joi.array().items(Joi.string().uuid().required()).default(null) }).required() }).required() diff --git a/src/services/PaymentSchedulerService.js b/src/services/PaymentSchedulerService.js index 666c66bb..b07694e4 100644 --- a/src/services/PaymentSchedulerService.js +++ b/src/services/PaymentSchedulerService.js @@ -123,7 +123,7 @@ async function processPayment (workPeriodPayment) { return processResult.SUCCESS } catch (err) { logger.logFullError(err, { component: 'PaymentSchedulerService', context: 'processPayment' }) - const statusDetails = { errorMessage: err.message, errorCode: _.get(err, 'status', -1), retry: _.get(err, 'retry', -1), step: _.get(err, 'step'), challengeId: paymentScheduler ? paymentScheduler.challengeId : null } + const statusDetails = { errorMessage: extractErrorMessage(err), errorCode: _.get(err, 'status', -1), retry: _.get(err, 'retry', -1), step: _.get(err, 'step'), challengeId: paymentScheduler ? paymentScheduler.challengeId : null } const oldValue = workPeriodPayment.toJSON() // If payment processing failed Work Periods Payment "status" should be changed to "failed" and populate "statusDetails" field with error details in JSON format. const updated = await workPeriodPayment.update({ statusDetails, status: 'failed' }) @@ -131,7 +131,7 @@ async function processPayment (workPeriodPayment) { await postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue }) if (paymentScheduler) { - await paymentScheduler.update({ step: PaymentSchedulerStatus.CLOSE_CHALLENGE, userId: paymentScheduler.userId, status: 'failed' }) + await paymentScheduler.update({ step: _.get(err, 'step'), userId: paymentScheduler.userId, status: 'failed' }) } localLogger.error(`Processed workPeriodPayment ${workPeriodPayment.id} failed`, 'processPayment') return processResult.FAIL @@ -332,6 +332,19 @@ async function withRetry (func, argArr, predictFunc, step) { } } +/** + * Extract error message from throwed error object + * @param {object} err the error object + * @returns {String} the error message + */ +function extractErrorMessage (err) { + return _.get(err, 'response.body.result.content.message') || + _.get(err, 'response.body.message') || + _.get(err, 'message') || + _.get(err, 'response.res.statusMessage') || + err.toString() +} + module.exports = { processScheduler } diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 9204eed5..cabadca0 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -29,7 +29,8 @@ const cachedModelFields = _cacheModelFields() function _cacheModelFields () { const resourceBookingFields = _.keys(ResourceBooking.rawAttributes) const workPeriodFields = _.map(_.keys(WorkPeriod.rawAttributes), key => `workPeriods.${key}`) - return [...resourceBookingFields, 'workPeriods', ...workPeriodFields] + const workPeriodPaymentFields = _.map(_.keys(WorkPeriodPayment.rawAttributes), key => `workPeriods.payments.${key}`) + return [...resourceBookingFields, 'workPeriods', ...workPeriodFields, 'workPeriods.payments', ...workPeriodPaymentFields] } /** @@ -42,6 +43,16 @@ function _checkUserScopesForGetWorkPeriods (currentUser) { return currentUser.isMachine && helper.checkIfExists(getWorkPeriodsScopes, currentUser.scopes) } +/** + * Check user scopes for getting workPeriodPayments + * @param {Object} currentUser the user who perform this operation. + * @returns {Boolean} true if user is machine and has read/all workPeriodPayment scopes + */ +function _checkUserScopesForGetWorkPeriodPayments (currentUser) { + const getWorkPeriodPaymentsScopes = [constants.Scopes.READ_WORK_PERIOD_PAYMENT, constants.Scopes.ALL_WORK_PERIOD_PAYMENT] + return currentUser.isMachine && helper.checkIfExists(getWorkPeriodPaymentsScopes, currentUser.scopes) +} + /** * Evaluates the criterias and returns the fields * to be returned as a result of GET endpoints @@ -51,25 +62,32 @@ function _checkUserScopesForGetWorkPeriods (currentUser) { * @returns {Array} result.include field names to include * @returns {Array} result.fieldsRB ResourceBooking field names to include * @returns {Array} result.fieldsWP WorkPeriod field names to include + * @returns {Array} result.fieldsWPP WorkPeriodPayment field names to include * @returns {Array} result.excludeRB ResourceBooking field names to exclude * @returns {Array} result.excludeWP WorkPeriod field names to exclude * @returns {Boolean} result.regularUser is current user a regular user? * @returns {Boolean} result.allWorkPeriods will all WorkPeriod fields be returned? * @returns {Boolean} result.withWorkPeriods does fields include any WorkPeriod field? + * @returns {Boolean} result.allWorkPeriodPayments will all WorkPeriodPayment fields be returned? + * @returns {Boolean} result.withWorkPeriodPayments does fields include any WorkPeriodPayment field? * @returns {Boolean} result.sortByWP will the sorting be done by WorkPeriod field? + * @throws {BadRequestError} + * @throws {ForbiddenError} */ function _checkCriteriaAndGetFields (currentUser, criteria) { const result = { include: [], fieldsRB: [], fieldsWP: [], + fieldsWPP: [], excludeRB: [], - excludeWP: [] + excludeWP: [], + excludeWPP: [] } const fields = criteria.fields const sort = criteria.sortBy const onlyResourceBooking = _.isUndefined(fields) - const query = onlyResourceBooking ? [] : _.split(fields, ',') + const query = onlyResourceBooking ? [] : _.uniq(_.filter(_.map(_.split(fields, ','), _.trim), field => !_.isEmpty(field))) const notAllowedFields = _.difference(query, cachedModelFields) // Check if fields criteria has a field name that RB or WP models don't have if (notAllowedFields.length > 0) { @@ -79,12 +97,17 @@ function _checkCriteriaAndGetFields (currentUser, criteria) { result.regularUser = !currentUser.hasManagePermission && !currentUser.isMachine && !currentUser.isConnectManager // Check if all WorkPeriod fields will be returned result.allWorkPeriods = _.some(query, q => q === 'workPeriods') + // Check if all WorkPeriodPayment fields will be returned + result.allWorkPeriodPayments = result.allWorkPeriods || _.some(query, q => q === 'workPeriods.payments') // Split the fields criteria into ResourceBooking and WorkPeriod fields _.forEach(query, q => { - if (_.includes(q, '.')) { result.fieldsWP.push(q) } else if (q !== 'workPeriods') { result.fieldsRB.push(q) } + if (_.includes(q, 'payments.')) { result.fieldsWPP.push(q) } else if (q !== 'workPeriods.payments' && _.includes(q, '.')) { result.fieldsWP.push(q) } else if (q !== 'workPeriods' && q !== 'workPeriods.payments') { result.fieldsRB.push(q) } }) // Check if any WorkPeriod field will be returned - result.withWorkPeriods = result.allWorkPeriods || result.fieldsWP.length > 0 + result.withWorkPeriods = result.allWorkPeriods || result.fieldsWP.length > 0 || + result.allWorkPeriodPayments || result.fieldsWPP.length > 0 + // Check if any WorkPeriodPayment field will be returned + result.withWorkPeriodPayments = result.allWorkPeriodPayments || result.fieldsWPP.length > 0 // Extract the filters from criteria parameter let filters = _.filter(Object.keys(criteria), key => _.indexOf(['fromDb', 'fields', 'page', 'perPage', 'sortBy', 'sortOrder'], key) === -1) filters = _.map(filters, f => { @@ -94,20 +117,22 @@ function _checkCriteriaAndGetFields (currentUser, criteria) { }) const filterRB = [] const filterWP = [] - // Split the filters criteria into ResourceBooking and WorkPeriod filters - _.forEach(filters, q => { if (_.includes(q, '.')) { filterWP.push(q) } else { filterRB.push(q) } }) - // Check if filter criteria has any WorkPeriod filter - const filterHasWorkPeriods = filterWP.length > 0 + const filterWPP = [] + // Split the filters criteria into ResourceBooking, WorkPeriod and WorkPeriodPayment filters + _.forEach(filters, q => { if (_.includes(q, 'payments.')) { filterWPP.push(q) } else if (_.includes(q, '.')) { filterWP.push(q) } else { filterRB.push(q) } }) + // Check if filter criteria has any WorkPeriod or payments filter + const filterHasWorkPeriods = filterWP.length > 0 || filterWPP.length > 0 // Check if sorting will be done by WorkPeriod field result.sortByWP = _.split(sort, '.')[0] === 'workPeriods' // Check if the current user has the right to see the memberRate const canSeeMemberRate = currentUser.hasManagePermission || currentUser.isMachine - // If current user has no right to see the memberRate then it's excluded + // If current user has no right to see the memberRate then it's excluded. // "currentUser.isMachine" to be true is not enough to return "workPeriods.memberRate" // but returning "workPeriod" will be evaluated later if (!canSeeMemberRate) { result.excludeRB.push('paymentTotal') result.excludeWP.push('workPeriods.paymentTotal') + result.excludeWPP.push('workPeriods.payments') } // if "fields" is not included in cretia, then only ResourceBooking model will be returned // No further evaluation is required as long as the criteria does not include a WorkPeriod filter or a WorkPeriod sorting condition @@ -130,20 +155,28 @@ function _checkCriteriaAndGetFields (currentUser, criteria) { } // Check If it's tried to filter or sort by some field which should not be included as per rules of fields param if (_.difference(filterRB, result.fieldsRB).length > 0) { - throw new errors.BadRequestError('Can not filter or sort by some field which is not included in fields') + throw new errors.BadRequestError('Can not filter or sort by ResourceBooking field which is not included in fields') } // Check If it's tried to filter or sort by some field which should not be included as per rules of fields param if (!result.allWorkPeriods && _.difference(filterWP, result.fieldsWP).length > 0) { - throw new errors.BadRequestError('Can not filter or sort by some field which is not included in fields') + throw new errors.BadRequestError('Can not filter or sort by WorkPeriod field which is not included in fields') + } + // Check If it's tried to filter or sort by some field which should not be included as per rules of fields param + if (!result.allWorkPeriodPayments && _.difference(filterWPP, result.fieldsWPP).length > 0) { + throw new errors.BadRequestError('Can not filter by WorkPeriodPayment field which is not included in fields') } // Check if the current user has no right to see the memberRate and memberRate is included in fields parameter - if (!canSeeMemberRate && _.some(query, q => _.includes(['memberRate', 'workPeriods.paymentTotal'], q))) { - throw new errors.ForbiddenError('You don\'t have access to view memberRate and paymentTotal') + if (!canSeeMemberRate && _.some(query, q => _.includes(['memberRate', 'workPeriods.paymentTotal', 'workPeriods.payments'], q))) { + throw new errors.ForbiddenError('You don\'t have access to view memberRate, paymentTotal and payments') } // Check if the current user has no right to see the workPeriods and workPeriods is included in fields parameter if (currentUser.isMachine && result.withWorkPeriods && !_checkUserScopesForGetWorkPeriods(currentUser)) { throw new errors.ForbiddenError('You don\'t have access to view workPeriods') } + // Check if the current user has no right to see the workPeriodPayments and workPeriodPayments is included in fields parameter + if (currentUser.isMachine && result.withWorkPeriodPayments && !_checkUserScopesForGetWorkPeriodPayments(currentUser)) { + throw new errors.ForbiddenError('You don\'t have access to view workPeriodPayments') + } result.include.push(...query) return result } @@ -249,7 +282,7 @@ async function getResourceBooking (currentUser, id, criteria) { index: config.esConfig.ES_INDEX_RESOURCE_BOOKING, id, _source_includes: [...queryOpt.include], - _source_excludes: ['workPeriods.payments', ...queryOpt.excludeRB, ...queryOpt.excludeWP] + _source_excludes: [...queryOpt.excludeRB, ...queryOpt.excludeWP, ...queryOpt.excludeWPP] }) if (queryOpt.regularUser) { await _checkUserPermissionForGetResourceBooking(currentUser, resourceBooking.body._source.projectId) // check user permission @@ -266,11 +299,18 @@ async function getResourceBooking (currentUser, id, criteria) { } } logger.info({ component: 'ResourceBookingService', context: 'getResourceBooking', message: 'try to query db for data' }) - const resourceBooking = await ResourceBooking.findById(id, queryOpt) + let resourceBooking = await ResourceBooking.findById(id, queryOpt) + resourceBooking = resourceBooking.toJSON() + // omit workPeriod.id if fields criteria has no workPeriod field but have workPeriodPayment field + if (queryOpt.withWorkPeriods && !queryOpt.allWorkPeriods && (!queryOpt.fieldsWP || queryOpt.fieldsWP.length === 0)) { + if (_.isArray(resourceBooking.workPeriods)) { + resourceBooking.workPeriods = _.map(resourceBooking.workPeriods, wp => _.omit(wp, 'id')) + } + } if (queryOpt.regularUser) { await _checkUserPermissionForGetResourceBooking(currentUser, resourceBooking.projectId) // check user permission } - return resourceBooking.dataValues + return resourceBooking } getResourceBooking.schema = Joi.object().keys({ @@ -460,7 +500,7 @@ deleteResourceBooking.schema = Joi.object().keys({ * @param {Object} options the extra options to control the function * @returns {Object} the search result, contain total/page/perPage and result array */ -async function searchResourceBookings (currentUser, criteria, options = { returnAll: false }) { +async function searchResourceBookings (currentUser, criteria, options) { // Evaluate criteria and extract the fields to be included or excluded const queryOpt = _checkCriteriaAndGetFields(currentUser, criteria) // check user permission @@ -506,128 +546,161 @@ async function searchResourceBookings (currentUser, criteria, options = { return if (!criteria.sortOrder) { criteria.sortOrder = 'desc' } - try { - const esQuery = { - index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), - _source_includes: queryOpt.include, - _source_excludes: ['workPeriods.payments', ...queryOpt.excludeRB, ...queryOpt.excludeWP], - body: { - query: { - bool: { - must: [] - } - }, - from: (page - 1) * perPage, - size: perPage, - sort: [] + // this option to return data from DB is only for internal usage, and it cannot be passed from the endpoint + if (!options.returnFromDB) { + try { + const esQuery = { + index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + _source_includes: queryOpt.include, + _source_excludes: [...queryOpt.excludeRB, ...queryOpt.excludeWP, ...queryOpt.excludeWPP], + body: { + query: { + bool: { + must: [] + } + }, + from: (page - 1) * perPage, + size: perPage, + sort: [] + } + } + // change the date format to match with index schema + if (criteria.startDate) { + criteria.startDate = moment(criteria.startDate).format('YYYY-MM-DD') + } + if (criteria.endDate) { + criteria.endDate = moment(criteria.endDate).format('YYYY-MM-DD') } - } - // change the date format to match with index schema - if (criteria.startDate) { - criteria.startDate = moment(criteria.startDate).format('YYYY-MM-DD') - } - if (criteria.endDate) { - criteria.endDate = moment(criteria.endDate).format('YYYY-MM-DD') - } - if (criteria['workPeriods.startDate']) { - criteria['workPeriods.startDate'] = moment(criteria['workPeriods.startDate']).format('YYYY-MM-DD') - } - if (criteria['workPeriods.endDate']) { - criteria['workPeriods.endDate'] = moment(criteria['workPeriods.endDate']).format('YYYY-MM-DD') - } - const sort = { [criteria.sortBy === 'id' ? '_id' : criteria.sortBy]: { order: criteria.sortOrder } } - if (queryOpt.sortByWP) { - const nestedSortFilter = {} if (criteria['workPeriods.startDate']) { - nestedSortFilter.term = { 'workPeriods.startDate': criteria['workPeriods.startDate'] } - } else if (criteria['workPeriods.endDate']) { - nestedSortFilter.term = { 'workPeriods.endDate': criteria['workPeriods.endDate'] } + criteria['workPeriods.startDate'] = moment(criteria['workPeriods.startDate']).format('YYYY-MM-DD') } - sort[criteria.sortBy].nested = { path: 'workPeriods', filter: nestedSortFilter } - } - esQuery.body.sort.push(sort) - // Apply ResourceBooking filters - _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId']), (value, key) => { - esQuery.body.query.bool.must.push({ - term: { - [key]: { - value - } + if (criteria['workPeriods.endDate']) { + criteria['workPeriods.endDate'] = moment(criteria['workPeriods.endDate']).format('YYYY-MM-DD') + } + const sort = { [criteria.sortBy === 'id' ? '_id' : criteria.sortBy]: { order: criteria.sortOrder } } + if (queryOpt.sortByWP) { + const nestedSortFilter = {} + if (criteria['workPeriods.startDate']) { + nestedSortFilter.term = { 'workPeriods.startDate': criteria['workPeriods.startDate'] } + } else if (criteria['workPeriods.endDate']) { + nestedSortFilter.term = { 'workPeriods.endDate': criteria['workPeriods.endDate'] } } + sort[criteria.sortBy].nested = { path: 'workPeriods', filter: nestedSortFilter } + } + esQuery.body.sort.push(sort) + // Apply ResourceBooking filters + _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId']), (value, key) => { + esQuery.body.query.bool.must.push({ + term: { + [key]: { + value + } + } + }) }) - }) - // if criteria contains projectIds, filter projectId with this value - if (criteria.projectIds) { - esQuery.body.query.bool.filter = [{ - terms: { - projectId: criteria.projectIds - } - }] - } - // Apply WorkPeriod filters - const workPeriodFilters = _.pick(criteria, ['workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle']) - if (!_.isEmpty(workPeriodFilters)) { - const workPeriodsMust = [] - _.each(workPeriodFilters, (value, key) => { - if (key === 'workPeriods.paymentStatus') { - workPeriodsMust.push({ - terms: { - [key]: value + // if criteria contains projectIds, filter projectId with this value + if (criteria.projectIds) { + esQuery.body.query.bool.filter = [{ + terms: { + projectId: criteria.projectIds + } + }] + } + // Apply WorkPeriod and WorkPeriodPayment filters + const workPeriodFilters = _.pick(criteria, ['workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle']) + const workPeriodPaymentFilters = _.pick(criteria, ['workPeriods.payments.status', 'workPeriods.payments.days']) + if (!_.isEmpty(workPeriodFilters) || !_.isEmpty(workPeriodPaymentFilters)) { + const workPeriodsMust = [] + _.each(workPeriodFilters, (value, key) => { + if (key === 'workPeriods.paymentStatus') { + workPeriodsMust.push({ + terms: { + [key]: value + } + }) + } else { + workPeriodsMust.push({ + term: { + [key]: { + value + } + } + }) + } + }) + const workPeriodPaymentPath = [] + if (!_.isEmpty(workPeriodPaymentFilters)) { + const workPeriodPaymentsMust = [] + _.each(workPeriodPaymentFilters, (value, key) => { + workPeriodPaymentsMust.push({ + term: { + [key]: { + value + } + } + }) + }) + workPeriodPaymentPath.push({ + nested: { + path: 'workPeriods.payments', + query: { bool: { must: workPeriodPaymentsMust } } } }) - } else { - workPeriodsMust.push({ - term: { - [key]: { - value + } + esQuery.body.query.bool.must.push({ + nested: { + path: 'workPeriods', + query: { + bool: { + must: [...workPeriodsMust, ...workPeriodPaymentPath] } } + } + }) + } + logger.debug({ component: 'ResourceBookingService', context: 'searchResourceBookings', message: `Query: ${JSON.stringify(esQuery)}` }) + + const { body } = await esClient.search(esQuery) + const resourceBookings = _.map(body.hits.hits, '_source') + // ESClient will return ResourceBookings with it's all nested WorkPeriods + // We re-apply WorkPeriod filters except userHandle because all WPs share same userHandle + if (!_.isEmpty(workPeriodFilters) || !_.isEmpty(workPeriodPaymentFilters)) { + _.each(resourceBookings, r => { + r.workPeriods = _.filter(r.workPeriods, wp => { + return _.every(_.omit(workPeriodFilters, 'workPeriods.userHandle'), (value, key) => { + key = key.split('.')[1] + if (key === 'paymentStatus') { + return _.includes(value, wp[key]) + } else { + return wp[key] === value + } + }) && _.every(workPeriodPaymentFilters, (value, key) => { + key = key.split('.')[2] + wp.payments = _.filter(wp.payments, payment => payment[key] === value) + return wp.payments.length > 0 + }) }) - } - }) - - esQuery.body.query.bool.must.push({ - nested: { - path: 'workPeriods', - query: { bool: { must: workPeriodsMust } } - } - }) - } - logger.debug({ component: 'ResourceBookingService', context: 'searchResourceBookings', message: `Query: ${JSON.stringify(esQuery)}` }) + }) + } - const { body } = await esClient.search(esQuery) - const resourceBookings = _.map(body.hits.hits, '_source') - // ESClient will return ResourceBookings with it's all nested WorkPeriods - // We re-apply WorkPeriod filters except userHandle because all WPs share same userHandle - _.each(_.omit(workPeriodFilters, 'workPeriods.userHandle'), (value, key) => { - key = key.split('.')[1] + // sort Work Periods inside Resource Bookings by startDate just for comfort output _.each(resourceBookings, r => { - r.workPeriods = _.filter(r.workPeriods, wp => { - if (key === 'paymentStatus') { - return _.includes(value, wp[key]) - } else { - return wp[key] === value - } - }) + if (_.isArray(r.workPeriods)) { + r.workPeriods = _.sortBy(r.workPeriods, ['startDate']) + } }) - }) - // sort Work Periods inside Resource Bookings by startDate just for comfort output - _.each(resourceBookings, r => { - if (_.isArray(r.workPeriods)) { - r.workPeriods = _.sortBy(r.workPeriods, ['startDate']) + return { + total: body.hits.total.value, + page, + perPage, + result: resourceBookings } - }) - - return { - total: body.hits.total.value, - page, - perPage, - result: resourceBookings + } catch (err) { + logger.logFullError(err, { component: 'ResourceBookingService', context: 'searchResourceBookings' }) } - } catch (err) { - logger.logFullError(err, { component: 'ResourceBookingService', context: 'searchResourceBookings' }) } + logger.info({ component: 'ResourceBookingService', context: 'searchResourceBookings', message: 'fallback to DB query' }) const filter = { [Op.and]: [] } // Apply ResourceBooking filters @@ -658,7 +731,13 @@ async function searchResourceBookings (currentUser, criteria, options = { return }] // Select WorkPeriod fields if (!queryOpt.allWorkPeriods) { - queryCriteria.include[0].attributes = _.map(queryOpt.fieldsWP, f => _.split(f, '.')[1]) + if (queryOpt.fieldsWP && queryOpt.fieldsWP.length > 0) { + queryCriteria.include[0].attributes = _.map(queryOpt.fieldsWP, f => _.split(f, '.')[1]) + } else { + // we should include at least one workPeriod field + // if fields criteria has no workPeriod field but have workPeriodPayment field + queryCriteria.include[0].attributes = ['id'] + } } else if (queryOpt.excludeWP && queryOpt.excludeWP.length > 0) { queryCriteria.include[0].attributes = { exclude: _.map(queryOpt.excludeWP, f => _.split(f, '.')[1]) } } @@ -673,6 +752,30 @@ async function searchResourceBookings (currentUser, criteria, options = { return if (queryCriteria.include[0].where[Op.and].length > 0) { queryCriteria.include[0].required = true } + // Include WorkPeriodPayment Model + if (queryOpt.withWorkPeriodPayments) { + queryCriteria.include[0].include = [{ + model: WorkPeriodPayment, + as: 'payments', + required: false, + where: { [Op.and]: [] } + }] + // Select WorkPeriodPayment fields + if (!queryOpt.allWorkPeriodPayments) { + queryCriteria.include[0].include[0].attributes = _.map(queryOpt.fieldsWPP, f => _.split(f, '.')[2]) + } else if (queryOpt.excludeWPP && queryOpt.excludeWPP.length > 0) { + queryCriteria.include[0].include[0].attributes = { exclude: _.map(queryOpt.excludeWPP, f => _.split(f, '.')[2]) } + } + // Apply WorkPeriodPayment filters + _.each(_.pick(criteria, ['workPeriods.payments.status', 'workPeriods.payments.days']), (value, key) => { + key = key.split('.')[2] + queryCriteria.include[0].include[0].where[Op.and].push({ [key]: value }) + }) + if (queryCriteria.include[0].include[0].where[Op.and].length > 0) { + queryCriteria.include[0].required = true + queryCriteria.include[0].include[0].required = true + } + } } // Apply sorting criteria if (!queryOpt.sortByWP) { @@ -681,7 +784,16 @@ async function searchResourceBookings (currentUser, criteria, options = { return queryCriteria.subQuery = false queryCriteria.order = [[{ model: WorkPeriod, as: 'workPeriods' }, _.split(criteria.sortBy, '.')[1], `${criteria.sortOrder} NULLS LAST`]] } - const result = await ResourceBooking.findAll(queryCriteria) + const resultModel = await ResourceBooking.findAll(queryCriteria) + const result = _.map(resultModel, r => r.toJSON()) + // omit workPeriod.id if fields criteria has no workPeriod field but have workPeriodPayment field + if (queryOpt.withWorkPeriods && !queryOpt.allWorkPeriods && (!queryOpt.fieldsWP || queryOpt.fieldsWP.length === 0)) { + _.each(result, r => { + if (_.isArray(r.workPeriods)) { + r.workPeriods = _.map(r.workPeriods, wp => _.omit(wp, 'id')) + } + }) + } // sort Work Periods inside Resource Bookings by startDate just for comfort output _.each(result, r => { if (_.isArray(r.workPeriods)) { @@ -731,9 +843,17 @@ searchResourceBookings.schema = Joi.object().keys({ ), 'workPeriods.startDate': Joi.date().format('YYYY-MM-DD'), 'workPeriods.endDate': Joi.date().format('YYYY-MM-DD'), - 'workPeriods.userHandle': Joi.string() + 'workPeriods.userHandle': Joi.string(), + 'workPeriods.payments.status': Joi.workPeriodPaymentStatus(), + 'workPeriods.payments.days': Joi.number().integer().min(0).max(5) }).required(), - options: Joi.object() + options: Joi.object().keys({ + returnAll: Joi.boolean().default(false), + returnFromDB: Joi.boolean().default(false) + }).default({ + returnAll: false, + returnFromDB: false + }) }).required() module.exports = { diff --git a/src/services/RoleService.js b/src/services/RoleService.js index bbd2c141..765d0f5c 100644 --- a/src/services/RoleService.js +++ b/src/services/RoleService.js @@ -74,7 +74,7 @@ async function _checkIfSameNamedRoleExists (roleName) { * @param {Boolean} fromDb flag if query db for data or not * @returns {Object} the role */ -async function getRole (currentUser, id, fromDb = false) { +async function getRole (id, fromDb = false) { if (!fromDb) { try { const role = await esClient.get({ @@ -95,7 +95,6 @@ async function getRole (currentUser, id, fromDb = false) { } getRole.schema = Joi.object().keys({ - currentUser: Joi.object().required(), id: Joi.string().uuid().required(), fromDb: Joi.boolean() }).required() @@ -230,7 +229,7 @@ deleteRole.schema = Joi.object().keys({ * @param {Object} criteria the search criteria * @returns {Object} the search result */ -async function searchRoles (currentUser, criteria) { +async function searchRoles (criteria) { // clean skill names and convert into an array criteria.skillsList = _.filter(_.map(_.split(criteria.skillsList, ','), skill => _.trim(skill)), skill => !_.isEmpty(skill)) try { @@ -291,7 +290,6 @@ async function searchRoles (currentUser, criteria) { } searchRoles.schema = Joi.object().keys({ - currentUser: Joi.object().required(), criteria: Joi.object().keys({ skillsList: Joi.string(), keyword: Joi.string() diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 36797330..bc236cf1 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -16,6 +16,7 @@ const HttpStatus = require('http-status-codes') const { Op } = require('sequelize') const models = require('../models') const stopWords = require('../../data/stopWords.json') +const { getAuditM2Muser } = require('../common/helper') const Role = models.Role const RoleSearchRequest = models.RoleSearchRequest const topcoderSkills = {} @@ -750,11 +751,16 @@ getMe.schema = Joi.object() * @returns {Object} the created project */ async function roleSearchRequest (currentUser, data) { + // if currentUser is undefined then set to machine + if (_.isUndefined(currentUser)) { + currentUser = getAuditM2Muser() + } let role // if roleId is provided then find role with given id. if (!_.isUndefined(data.roleId)) { role = await Role.findById(data.roleId) role = role.toJSON() + role.skillsMatch = 1; // if skills is provided then use skills to find role } else if (!_.isUndefined(data.skills)) { // validate given skillIds and convert them into skill names @@ -779,7 +785,7 @@ async function roleSearchRequest (currentUser, data) { roleSearchRequest.schema = Joi.object() .keys({ - currentUser: Joi.object().required(), + currentUser: Joi.object(), data: Joi.object().keys({ roleId: Joi.string().uuid(), jobDescription: Joi.string().max(255), @@ -793,28 +799,29 @@ roleSearchRequest.schema = Joi.object() * @returns {Role} the best matching Role */ async function getRoleBySkills (skills) { + const lowerCaseSkills = skills.map(skill => skill.toLowerCase()) // find all roles which includes any of the given skills const queryCriteria = { - where: { listOfSkills: { [Op.overlap]: skills } }, + where: { listOfSkills: { [Op.overlap]: lowerCaseSkills } }, raw: true } const roles = await Role.findAll(queryCriteria) if (roles.length > 0) { let result = _.each(roles, role => { // calculate each found roles matching rate - role.matchingRate = _.intersection(role.listOfSkills, skills).length / skills.length + role.skillsMatch = _.intersection(role.listOfSkills, lowerCaseSkills).length / skills.length // each role can have multiple rates, get the maximum of global rates role.maxGlobal = _.maxBy(role.rates, 'global').global }) - // sort roles by matchingRate, global rate and name - result = _.orderBy(result, ['matchingRate', 'maxGlobal', 'name'], ['desc', 'desc', 'asc']) - if (result[0].matchingRate >= config.ROLE_MATCHING_RATE) { + // sort roles by skillsMatch, global rate and name + result = _.orderBy(result, ['skillsMatch', 'maxGlobal', 'name'], ['desc', 'desc', 'asc']) + if (result[0].skillsMatch >= config.ROLE_MATCHING_RATE) { // return the 1st role - return _.omit(result[0], ['matchingRate', 'maxGlobal']) + return _.omit(result[0], ['maxGlobal']) } } - // if no matching role found then return Niche role or empty object - return await Role.findOne({ where: { name: { [Op.iLike]: 'Niche' } } }) || {} + // if no matching role found then return Custom role or empty object + return await Role.findOne({ where: { name: { [Op.iLike]: 'Custom' } } }) || {} } getRoleBySkills.schema = Joi.object() diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index 576d934d..72b94d44 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -8,6 +8,7 @@ const config = require('config') const HttpStatus = require('http-status-codes') const { Op } = require('sequelize') const uuid = require('uuid') +const moment = require('moment') const helper = require('../common/helper') const logger = require('../common/logger') const errors = require('../common/errors') @@ -210,7 +211,7 @@ async function updateWorkPeriodPayment (currentUser, id, data) { if (oldValue.status !== 'failed') { throw new errors.BadRequestError(`You cannot schedule a WorkPeriodPayment which is ${oldValue.status}`) } - const workPeriod = WorkPeriod.findById(workPeriodPayment.workPeriodId) + const workPeriod = await WorkPeriod.findById(workPeriodPayment.workPeriodId) // we con't check if paymentStatus is 'completed' // because paymentStatus can be in-progress when daysWorked = daysPaid if (workPeriod.daysWorked === workPeriod.daysPaid) { @@ -379,6 +380,22 @@ searchWorkPeriodPayments.schema = Joi.object().keys({ async function createQueryWorkPeriodPayments (currentUser, criteria) { // check permission _checkUserPermissionForCRUWorkPeriodPayment(currentUser) + // Joi validation normalizes the dates back to ISO format + // so, we need to change the date format back to YYYY-MM-DD + if (criteria.query.startDate) { + criteria.query.startDate = moment(criteria.query.startDate).format('YYYY-MM-DD') + } + if (criteria.query.endDate) { + criteria.query.endDate = moment(criteria.query.endDate).format('YYYY-MM-DD') + } + if (criteria.query['workPeriods.startDate']) { + criteria.query['workPeriods.startDate'] = moment(criteria.query['workPeriods.startDate']).format('YYYY-MM-DD') + } + if (criteria.query['workPeriods.endDate']) { + criteria.query['workPeriods.endDate'] = moment(criteria.query['workPeriods.endDate']).format('YYYY-MM-DD') + } + // save query to return back + const rawQuery = _.cloneDeep(criteria.query) const createdBy = await helper.getUserId(currentUser.userId) const query = criteria.query if ((typeof query['workPeriods.paymentStatus']) === 'string') { @@ -388,11 +405,11 @@ async function createQueryWorkPeriodPayments (currentUser, criteria) { ['id', 'billingAccountId', 'memberRate', 'customerRate', 'workPeriods.id', 'workPeriods.resourceBookingId', 'workPeriods.daysWorked', 'workPeriods.daysPaid'], _.map(_.keys(query), k => k === 'projectIds' ? 'projectId' : k)) ), ',') - const searchResult = await searchResourceBookings(currentUser, _.extend({ fields, page: 1 }, query), { returnAll: true }) + const searchResult = await searchResourceBookings(currentUser, _.extend({ fields, page: 1 }, query), { returnAll: true, returnFromDB: true }) const wpArray = _.flatMap(searchResult.result, 'workPeriods') const resourceBookingMap = _.fromPairs(_.map(searchResult.result, rb => [rb.id, rb])) - const result = { total: wpArray.length, query, totalSuccess: 0, totalError: 0 } + const result = { total: wpArray.length, query: rawQuery, totalSuccess: 0, totalError: 0 } for (const wp of wpArray) { try { diff --git a/test/unit/common/ResourceBookingData.js b/test/unit/common/ResourceBookingData.js index 0970c73e..1f9535a1 100644 --- a/test/unit/common/ResourceBookingData.js +++ b/test/unit/common/ResourceBookingData.js @@ -1206,6 +1206,7 @@ const T18 = { }, criteria: { fromDb: true } } +T18.resourceBooking.value.toJSON = () => T18.resourceBooking.value.dataValues const T19 = { id: '520bb632-a02a-415e-9857-93b2ecbf7d60', criteria: { @@ -1223,7 +1224,7 @@ const T20 = { }, error: { httpStatus: 403, - message: 'You don\'t have access to view memberRate and paymentTotal' + message: 'You don\'t have access to view memberRate, paymentTotal and payments' } } const T21 = { @@ -1371,38 +1372,44 @@ const T24 = { const T25 = { resourceBookingFindAll: [ { - updatedBy: null, - endDate: '2020-10-27', - billingAccountId: 80000071, - userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', - jobId: '05232809-3693-44c1-a0cc-9a79f2672385', - rateType: 'hourly', - createdAt: '2021-05-08T18:35:16.368Z', - memberRate: 13.23, - createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', - customerRate: 13, - id: 'fbe133dd-0e36-4d0c-8197-49307b13ce75', - projectId: 17234, - startDate: '2020-09-27', - status: 'placed', - updatedAt: '2021-05-08T18:35:16.368Z' + dataValues: { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '05232809-3693-44c1-a0cc-9a79f2672385', + rateType: 'hourly', + createdAt: '2021-05-08T18:35:16.368Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: 'fbe133dd-0e36-4d0c-8197-49307b13ce75', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-05-08T18:35:16.368Z' + }, + toJSON: () => T25.resourceBookingFindAll[0].dataValues }, { - updatedBy: null, - endDate: '2020-10-27', - billingAccountId: 80000071, - userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', - jobId: '05232809-3693-44c1-a0cc-9a79f2672385', - rateType: 'hourly', - createdAt: '2021-05-08T18:47:37.268Z', - memberRate: 13.23, - createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', - customerRate: 13, - id: '60e99790-8da0-4596-badc-29a06feb78a0', - projectId: 17234, - startDate: '2020-09-27', - status: 'placed', - updatedAt: '2021-05-08T18:47:37.268Z' + dataValues: { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '05232809-3693-44c1-a0cc-9a79f2672385', + rateType: 'hourly', + createdAt: '2021-05-08T18:47:37.268Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: '60e99790-8da0-4596-badc-29a06feb78a0', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-05-08T18:47:37.268Z' + }, + toJSON: () => T25.resourceBookingFindAll[1].dataValues } ], resourceBookingCount: [{ id: 'fbe133dd-0e36-4d0c-8197-49307b13ce75', count: 1 }, @@ -1466,7 +1473,7 @@ const T27 = { }, error: { httpStatus: 403, - message: 'You don\'t have access to view memberRate and paymentTotal' + message: 'You don\'t have access to view memberRate, paymentTotal and payments' } } const T28 = { @@ -1517,14 +1524,14 @@ const T34 = { criteria: { fields: 'id,startDate,endDate,workPeriods', status: 'closed' }, error: { httpStatus: 400, - message: 'Can not filter or sort by some field which is not included in fields' + message: 'Can not filter or sort by ResourceBooking field which is not included in fields' } } const T35 = { criteria: { fields: 'id,startDate,endDate', 'workPeriods.paymentStatus': 'completed' }, error: { httpStatus: 400, - message: 'Can not filter or sort by some field which is not included in fields' + message: 'Can not filter or sort by WorkPeriod field which is not included in fields' } } const T36 = {