From dc1cfa43216d85b328b0697927e6087d11efa988 Mon Sep 17 00:00:00 2001 From: dengjun Date: Sat, 22 May 2021 16:16:26 +0800 Subject: [PATCH 01/23] Add JobIds to /jobs endpoint:30185451 --- ...coder-bookings-api.postman_collection.json | 109 +++++++++++++++++- docs/swagger.yaml | 65 +++++++++-- src/controllers/JobController.js | 3 + src/services/JobService.js | 23 +++- 4 files changed, 186 insertions(+), 14 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index a6afdc2b..c6d395bb 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "3f80d93d-6ca3-4645-970d-a9e533394e2e", + "_postman_id": "4b866040-1336-427b-9e20-56d809b87519", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -491,6 +491,113 @@ }, "response": [] }, + { + "name": "search jobs with request body", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"jobIds\": [\"{{jobId}}\",\"{{jobIdCreatedByM2M}}\"]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobs", + "host": [ + "{{URL}}" + ], + "path": [ + "jobs" + ], + "query": [ + { + "key": "page", + "value": "0", + "disabled": true + }, + { + "key": "perPage", + "value": "3", + "disabled": true + }, + { + "key": "sortBy", + "value": "id", + "disabled": true + }, + { + "key": "sortOrder", + "value": "asc", + "disabled": true + }, + { + "key": "projectId", + "value": "21", + "disabled": true + }, + { + "key": "externalId", + "value": "1212", + "disabled": true + }, + { + "key": "description", + "value": "Dummy", + "disabled": true + }, + { + "key": "startDate", + "value": "2020-09-27T04:17:23.131Z", + "disabled": true + }, + { + "key": "resourceType", + "value": "Dummy Resource Type", + "disabled": true + }, + { + "key": "skill", + "value": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "disabled": true + }, + { + "key": "rateType", + "value": "hourly", + "disabled": true + }, + { + "key": "status", + "value": "sourcing", + "disabled": true + }, + { + "key": "workload", + "value": "full-time", + "disabled": true + }, + { + "key": "title", + "value": "dummy", + "disabled": true + } + ] + } + }, + "response": [] + }, { "name": "search jobs with with m2m all", "request": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 0a9fe80d..3a6ce840 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -181,6 +181,11 @@ paths: type: string enum: ["sourcing", "in-review", "assigned", "closed", "cancelled"] description: The rate type. + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/JobSearchBody" responses: "200": description: OK @@ -3317,6 +3322,15 @@ components: type: string example: "topcoder user" description: "The user who updated the job last time.(Will get the user info from the token)" + JobSearchBody: + properties: + jobIds: + type: array + items: + type: string + format: uuid + description: "The array of job ids" + JobRequestBody: required: - projectId @@ -3517,7 +3531,18 @@ components: description: "The user id." status: type: string - enum: ["open", "placed", "selected", "client rejected - screening", "client rejected - interview", "rejected - other", "cancelled", "interview", "topcoder-rejected"] + enum: + [ + "open", + "placed", + "selected", + "client rejected - screening", + "client rejected - interview", + "rejected - other", + "cancelled", + "interview", + "topcoder-rejected", + ] description: "The job candidate status." default: open externalId: @@ -3632,7 +3657,15 @@ components: description: "Interview end time." status: type: string - enum: ["Scheduling", "Scheduled", "Requested for reschedule", "Rescheduled", "Completed", "Cancelled"] + enum: + [ + "Scheduling", + "Scheduled", + "Requested for reschedule", + "Rescheduled", + "Completed", + "Cancelled", + ] description: "The interview status." rescheduleUrl: type: string @@ -3678,7 +3711,15 @@ components: format: email status: type: string - enum: ["Scheduling", "Scheduled", "Requested for reschedule", "Rescheduled", "Completed", "Cancelled"] + enum: + [ + "Scheduling", + "Scheduled", + "Requested for reschedule", + "Rescheduled", + "Completed", + "Cancelled", + ] default: "Scheduling" description: "The interview status." UpdateInterviewRequestBody: @@ -3737,7 +3778,15 @@ components: description: "Interview end time." status: type: string - enum: ["Scheduling", "Scheduled", "Requested for reschedule", "Rescheduled", "Completed", "Cancelled"] + enum: + [ + "Scheduling", + "Scheduled", + "Requested for reschedule", + "Rescheduled", + "Completed", + "Cancelled", + ] description: "The interview status." rescheduleUrl: type: string @@ -3845,7 +3894,7 @@ components: billingAccountId: type: integer example: 80000071 - description: 'the billing account id for payments' + description: "the billing account id for payments" createdAt: type: string format: date-time @@ -3913,7 +3962,7 @@ components: billingAccountId: type: integer example: 80000071 - description: 'the billing account id for payments' + description: "the billing account id for payments" ResourceBookingPatchRequestBody: properties: status: @@ -3946,7 +3995,7 @@ components: billingAccountId: type: integer example: 80000071 - description: 'the billing account id for payments' + description: "the billing account id for payments" WorkPeriod: required: - id @@ -4130,7 +4179,7 @@ components: billingAccountId: type: integer example: 80000071 - description: 'the billing account id for payments' + description: "the billing account id for payments" createdAt: type: string format: date-time diff --git a/src/controllers/JobController.js b/src/controllers/JobController.js index 14f5cfc5..b7cad958 100644 --- a/src/controllers/JobController.js +++ b/src/controllers/JobController.js @@ -57,6 +57,9 @@ async function deleteJob (req, res) { * @param res the response */ async function searchJobs (req, res) { + if (req.body && req.body.jobIds) { + req.query.jobIds = req.body.jobIds + } const result = await service.searchJobs(req.authUser, req.query) helper.setResHeaders(req, res, result) res.send(result.result) diff --git a/src/services/JobService.js b/src/services/JobService.js index 7d855bd0..61685901 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -344,7 +344,8 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } body: { query: { bool: { - must: [] + must: [], + filter: [] } }, from: (page - 1) * perPage, @@ -393,11 +394,19 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } }) // If criteria contains projectIds, filter projectId with this value if (criteria.projectIds) { - esQuery.body.query.bool.filter = [{ + esQuery.body.query.bool.filter.push({ terms: { projectId: criteria.projectIds } - }] + }) + } + // if criteria contains jobIds, filter jobIds with this value + if (criteria.jobIds && criteria.jobIds.length > 0) { + esQuery.body.query.bool.filter.push({ + terms: { + _id: criteria.jobIds + } + }) } logger.debug({ component: 'JobService', context: 'searchJobs', message: `Query: ${JSON.stringify(esQuery)}` }) @@ -422,7 +431,7 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } logger.logFullError(err, { component: 'JobService', context: 'searchJobs' }) } logger.info({ component: 'JobService', context: 'searchJobs', message: 'fallback to DB query' }) - const filter = {} + const filter = { [Op.and]: [] } _.each(_.pick(criteria, [ 'projectId', 'externalId', @@ -449,6 +458,9 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } [Op.contains]: [criteria.skills] } } + if (criteria.jobIds && criteria.jobIds.length > 0) { + filter[Op.and].push({ id: criteria.jobIds }) + } const jobs = await Job.findAll({ where: filter, offset: ((page - 1) * perPage), @@ -486,7 +498,8 @@ searchJobs.schema = Joi.object().keys({ rateType: Joi.rateType(), workload: Joi.workload(), status: Joi.jobStatus(), - projectIds: Joi.array().items(Joi.number().integer()).single() + projectIds: Joi.array().items(Joi.number().integer()).single(), + jobIds: Joi.array().items(Joi.string().uuid()) }).required(), options: Joi.object() }).required() From 8b55765ea32bf94debec6a9e27cc4e6f223058dd Mon Sep 17 00:00:00 2001 From: eisbilir Date: Sat, 22 May 2021 21:03:45 +0300 Subject: [PATCH 02/23] fix: checking if member of project --- src/services/WorkPeriodService.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index 19346ff3..fc750bd7 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -177,8 +177,9 @@ async function getWorkPeriod (currentUser, id, fromDb = false) { if (!resourceBooking.body.hits.total.value) { throw new errors.NotFoundError() } - await _checkUserPermissionForGetWorkPeriod(currentUser, resourceBooking.body.hits.hits[0]._source.workPeriods.projectId) // check user permission - return _.find(resourceBooking.body.hits.hits[0]._source.workPeriods, { id }) + const workPeriod = _.find(resourceBooking.body.hits.hits[0]._source.workPeriods, { id }) + await _checkUserPermissionForGetWorkPeriod(currentUser, workPeriod.projectId) // check user permission + return workPeriod } catch (err) { if (helper.isDocumentMissingException(err)) { throw new errors.NotFoundError(`id: ${id} "WorkPeriod" not found`) From 3a2a3d39b9600bb4a134766143514b6aa8714af2 Mon Sep 17 00:00:00 2001 From: Michael Baghel Date: Mon, 24 May 2021 23:11:45 +0400 Subject: [PATCH 03/23] Created new route for creating a team/project --- src/common/helper.js | 1327 ++++++++++++++++++----------- src/controllers/TeamController.js | 84 +- src/routes/TeamRoutes.js | 52 +- src/services/TeamService.js | 693 +++++++++------ 4 files changed, 1317 insertions(+), 839 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 234e96fe..b8dc9c5a 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -2,63 +2,103 @@ * This file defines helper methods */ -const fs = require('fs') -const querystring = require('querystring') -const Confirm = require('prompt-confirm') -const Bottleneck = require('bottleneck') -const AWS = require('aws-sdk') -const config = require('config') -const HttpStatus = require('http-status-codes') -const _ = require('lodash') -const request = require('superagent') -const elasticsearch = require('@elastic/elasticsearch') -const { ResponseError: ESResponseError } = require('@elastic/elasticsearch/lib/errors') -const errors = require('../common/errors') -const logger = require('./logger') -const models = require('../models') -const eventDispatcher = require('./eventDispatcher') -const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') -const moment = require('moment') +const fs = require('fs'); +const querystring = require('querystring'); +const Confirm = require('prompt-confirm'); +const Bottleneck = require('bottleneck'); +const AWS = require('aws-sdk'); +const config = require('config'); +const HttpStatus = require('http-status-codes'); +const _ = require('lodash'); +const request = require('superagent'); +const elasticsearch = require('@elastic/elasticsearch'); +const { + ResponseError: ESResponseError, +} = require('@elastic/elasticsearch/lib/errors'); +const errors = require('../common/errors'); +const logger = require('./logger'); +const models = require('../models'); +const eventDispatcher = require('./eventDispatcher'); +const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper'); +const moment = require('moment'); const localLogger = { - debug: (message) => logger.debug({ component: 'helper', context: message.context, message: message.message }), - error: (message) => logger.error({ component: 'helper', context: message.context, message: message.message }), - info: (message) => logger.info({ component: 'helper', context: message.context, message: message.message }) -} - -AWS.config.region = config.esConfig.AWS_REGION - -const m2mAuth = require('tc-core-library-js').auth.m2m - -const m2m = m2mAuth(_.pick(config, ['AUTH0_URL', 'AUTH0_AUDIENCE', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', 'AUTH0_PROXY_SERVER_URL'])) + debug: (message) => + logger.debug({ + component: 'helper', + context: message.context, + message: message.message, + }), + error: (message) => + logger.error({ + component: 'helper', + context: message.context, + message: message.message, + }), + info: (message) => + logger.info({ + component: 'helper', + context: message.context, + message: message.message, + }), +}; + +AWS.config.region = config.esConfig.AWS_REGION; + +const m2mAuth = require('tc-core-library-js').auth.m2m; + +const m2m = m2mAuth( + _.pick(config, [ + 'AUTH0_URL', + 'AUTH0_AUDIENCE', + 'AUTH0_CLIENT_ID', + 'AUTH0_CLIENT_SECRET', + 'AUTH0_PROXY_SERVER_URL', + ]) +); const m2mForUbahn = m2mAuth({ AUTH0_AUDIENCE: config.AUTH0_AUDIENCE_UBAHN, - ..._.pick(config, ['AUTH0_URL', 'TOKEN_CACHE_TIME', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', 'AUTH0_PROXY_SERVER_URL']) -} -) + ..._.pick(config, [ + 'AUTH0_URL', + 'TOKEN_CACHE_TIME', + 'AUTH0_CLIENT_ID', + 'AUTH0_CLIENT_SECRET', + 'AUTH0_PROXY_SERVER_URL', + ]), +}); -let busApiClient +let busApiClient; /** * Get bus api client. * * @returns {Object} the bus api client */ -function getBusApiClient () { +function getBusApiClient() { if (busApiClient) { - return busApiClient + return busApiClient; } - busApiClient = busApi(_.pick(config, ['AUTH0_URL', 'AUTH0_AUDIENCE', 'TOKEN_CACHE_TIME', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', 'BUSAPI_URL', 'KAFKA_ERROR_TOPIC', 'AUTH0_PROXY_SERVER_URL']) - ) - return busApiClient + busApiClient = busApi( + _.pick(config, [ + 'AUTH0_URL', + 'AUTH0_AUDIENCE', + 'TOKEN_CACHE_TIME', + 'AUTH0_CLIENT_ID', + 'AUTH0_CLIENT_SECRET', + 'BUSAPI_URL', + 'KAFKA_ERROR_TOPIC', + 'AUTH0_PROXY_SERVER_URL', + ]) + ); + return busApiClient; } // ES Client mapping -const esClients = {} +const esClients = {}; // The es index property mapping -const esIndexPropertyMapping = {} +const esIndexPropertyMapping = {}; esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { projectId: { type: 'integer' }, externalId: { type: 'keyword' }, @@ -76,8 +116,8 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } -} + updatedBy: { type: 'keyword' }, +}; esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { jobId: { type: 'keyword' }, userId: { type: 'keyword' }, @@ -110,14 +150,14 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, updatedBy: { type: 'keyword' }, - deletedAt: { type: 'date' } - } + deletedAt: { type: 'date' }, + }, }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } -} + updatedBy: { type: 'keyword' }, +}; esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { projectId: { type: 'integer' }, userId: { type: 'keyword' }, @@ -155,32 +195,32 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } - } + updatedBy: { type: 'keyword' }, + }, }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } - } + updatedBy: { type: 'keyword' }, + }, }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } -} + updatedBy: { type: 'keyword' }, +}; /** * Get the first parameter from cli arguments */ -function getParamFromCliArgs () { - const filteredArgs = process.argv.filter(arg => !arg.includes('--')) +function getParamFromCliArgs() { + const filteredArgs = process.argv.filter((arg) => !arg.includes('--')); if (filteredArgs.length > 2) { - return filteredArgs[2] + return filteredArgs[2]; } - return null + return null; } /** @@ -188,18 +228,18 @@ function getParamFromCliArgs () { * @param {string} promptQuery the query to ask the user * @param {function} cb the callback function */ -async function promptUser (promptQuery, cb) { +async function promptUser(promptQuery, cb) { if (process.argv.includes('--force')) { - await cb() - return + await cb(); + return; } - const prompt = new Confirm(promptQuery) + const prompt = new Confirm(promptQuery); prompt.ask(async (answer) => { if (answer) { - await cb() + await cb(); } - }) + }); } /** @@ -208,20 +248,23 @@ async function promptUser (promptQuery, cb) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function createIndex (index, logger, esClient = null) { +async function createIndex(index, logger, esClient = null) { if (!esClient) { - esClient = getESClient() + esClient = getESClient(); } await esClient.indices.create({ index, body: { mappings: { - properties: esIndexPropertyMapping[index] - } - } - }) - logger.info({ component: 'createIndex', message: `ES Index ${index} creation succeeded!` }) + properties: esIndexPropertyMapping[index], + }, + }, + }); + logger.info({ + component: 'createIndex', + message: `ES Index ${index} creation succeeded!`, + }); } /** @@ -230,105 +273,133 @@ async function createIndex (index, logger, esClient = null) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function deleteIndex (index, logger, esClient = null) { +async function deleteIndex(index, logger, esClient = null) { if (!esClient) { - esClient = getESClient() + esClient = getESClient(); } - await esClient.indices.delete({ index }) - logger.info({ component: 'deleteIndex', message: `ES Index ${index} deletion succeeded!` }) + await esClient.indices.delete({ index }); + logger.info({ + component: 'deleteIndex', + message: `ES Index ${index} deletion succeeded!`, + }); } /** * Split data into bulks * @param {Array} data the array of data to split */ -function getBulksFromDocuments (data) { - const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6 - const bulks = [] - let documentIndex = 0 - let currentBulkSize = 0 - let currentBulk = [] +function getBulksFromDocuments(data) { + const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6; + const bulks = []; + let documentIndex = 0; + let currentBulkSize = 0; + let currentBulk = []; while (true) { // break loop when parsed all documents if (documentIndex >= data.length) { - bulks.push(currentBulk) - break + bulks.push(currentBulk); + break; } // check if current document size is greater than the max bulk size, if so, throw error - const currentDocumentSize = Buffer.byteLength(JSON.stringify(data[documentIndex]), 'utf-8') + const currentDocumentSize = Buffer.byteLength( + JSON.stringify(data[documentIndex]), + 'utf-8' + ); if (maxBytes < currentDocumentSize) { - throw new Error(`Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.`) + throw new Error( + `Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.` + ); } - if (currentBulkSize + currentDocumentSize > maxBytes || - currentBulk.length >= config.get('esConfig.MAX_BULK_NUM_DOCUMENTS')) { + if ( + currentBulkSize + currentDocumentSize > maxBytes || + currentBulk.length >= config.get('esConfig.MAX_BULK_NUM_DOCUMENTS') + ) { // if adding the current document goes over the max bulk size OR goes over max number of docs // then push the current bulk to bulks array and reset the current bulk - bulks.push(currentBulk) - currentBulk = [] - currentBulkSize = 0 + bulks.push(currentBulk); + currentBulk = []; + currentBulkSize = 0; } else { // otherwise, add document to current bulk - currentBulk.push(data[documentIndex]) - currentBulkSize += currentDocumentSize - documentIndex++ + currentBulk.push(data[documentIndex]); + currentBulkSize += currentDocumentSize; + documentIndex++; } } - return bulks + return bulks; } /** -* Index records in bulk -* @param {Object | String} modelOpts the model name in db, or model options -* @param {Object} indexName the index name -* @param {Object} logger the logger object -*/ -async function indexBulkDataToES (modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName - const include = _.get(modelOpts, 'include', []) + * Index records in bulk + * @param {Object | String} modelOpts the model name in db, or model options + * @param {Object} indexName the index name + * @param {Object} logger the logger object + */ +async function indexBulkDataToES(modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; + const include = _.get(modelOpts, 'include', []); - logger.info({ component: 'indexBulkDataToES', message: `Reindexing of ${modelName}s started!` }) + logger.info({ + component: 'indexBulkDataToES', + message: `Reindexing of ${modelName}s started!`, + }); - const esClient = getESClient() + const esClient = getESClient(); // clear index - const indexExistsRes = await esClient.indices.exists({ index: indexName }) + const indexExistsRes = await esClient.indices.exists({ index: indexName }); if (indexExistsRes.statusCode !== 404) { - await deleteIndex(indexName, logger, esClient) + await deleteIndex(indexName, logger, esClient); } - await createIndex(indexName, logger, esClient) + await createIndex(indexName, logger, esClient); // get data from db - logger.info({ component: 'indexBulkDataToES', message: 'Getting data from database' }) - const model = models[modelName] - const data = await model.findAll({ include }) - const rawObjects = _.map(data, r => r.toJSON()) + logger.info({ + component: 'indexBulkDataToES', + message: 'Getting data from database', + }); + const model = models[modelName]; + const data = await model.findAll({ include }); + const rawObjects = _.map(data, (r) => r.toJSON()); if (_.isEmpty(rawObjects)) { - logger.info({ component: 'indexBulkDataToES', message: `No data in database for ${modelName}` }) - return + logger.info({ + component: 'indexBulkDataToES', + message: `No data in database for ${modelName}`, + }); + return; } - const bulks = getBulksFromDocuments(rawObjects) + const bulks = getBulksFromDocuments(rawObjects); - const startTime = Date.now() - let doneCount = 0 + const startTime = Date.now(); + let doneCount = 0; for (const bulk of bulks) { // send bulk to esclient - const body = bulk.flatMap(doc => [{ index: { _index: indexName, _id: doc.id } }, doc]) - await esClient.bulk({ refresh: true, body }) - doneCount += bulk.length + const body = bulk.flatMap((doc) => [ + { index: { _index: indexName, _id: doc.id } }, + doc, + ]); + await esClient.bulk({ refresh: true, body }); + doneCount += bulk.length; // log metrics - const timeSpent = Date.now() - startTime - const avgTimePerDocument = timeSpent / doneCount - const estimatedLength = (avgTimePerDocument * data.length) - const timeLeft = (startTime + estimatedLength) - Date.now() + const timeSpent = Date.now() - startTime; + const avgTimePerDocument = timeSpent / doneCount; + const estimatedLength = avgTimePerDocument * data.length; + const timeLeft = startTime + estimatedLength - Date.now(); logger.info({ component: 'indexBulkDataToES', - message: `Processed ${doneCount} of ${data.length} documents, average time per document ${formatTime(avgTimePerDocument)}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime(timeLeft)}` - }) + message: `Processed ${doneCount} of ${ + data.length + } documents, average time per document ${formatTime( + avgTimePerDocument + )}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime( + timeLeft + )}`, + }); } } @@ -339,24 +410,36 @@ async function indexBulkDataToES (modelOpts, indexName, logger) { * @param {string} id the job id * @param {Object} logger the logger object */ -async function indexDataToEsById (id, modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName - const include = _.get(modelOpts, 'include', []) - - logger.info({ component: 'indexDataToEsById', message: `Reindexing of ${modelName} with id ${id} started!` }) - const esClient = getESClient() - - logger.info({ component: 'indexDataToEsById', message: 'Getting data from database' }) - const model = models[modelName] - - const data = await model.findById(id, include) - logger.info({ component: 'indexDataToEsById', message: 'Indexing data into Elasticsearch' }) +async function indexDataToEsById(id, modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; + const include = _.get(modelOpts, 'include', []); + + logger.info({ + component: 'indexDataToEsById', + message: `Reindexing of ${modelName} with id ${id} started!`, + }); + const esClient = getESClient(); + + logger.info({ + component: 'indexDataToEsById', + message: 'Getting data from database', + }); + const model = models[modelName]; + + const data = await model.findById(id, include); + logger.info({ + component: 'indexDataToEsById', + message: 'Indexing data into Elasticsearch', + }); await esClient.index({ index: indexName, id: id, - body: data.dataValues - }) - logger.info({ component: 'indexDataToEsById', message: 'Indexing complete!' }) + body: data.dataValues, + }); + logger.info({ + component: 'indexDataToEsById', + message: 'Indexing complete!', + }); } /** @@ -365,50 +448,68 @@ async function indexDataToEsById (id, modelOpts, indexName, logger) { * @param {Array} dataModels the data models to import * @param {Object} logger the logger object */ -async function importData (pathToFile, dataModels, logger) { +async function importData(pathToFile, dataModels, logger) { // check if file exists if (!fs.existsSync(pathToFile)) { - throw new Error(`File with path ${pathToFile} does not exist`) + throw new Error(`File with path ${pathToFile} does not exist`); } // clear database - logger.info({ component: 'importData', message: 'Clearing database...' }) - await models.sequelize.sync({ force: true }) + logger.info({ component: 'importData', message: 'Clearing database...' }); + await models.sequelize.sync({ force: true }); - let transaction = null - let currentModelName = null + let transaction = null; + let currentModelName = null; try { // Start a transaction - transaction = await models.sequelize.transaction() - const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()) + transaction = await models.sequelize.transaction(); + const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()); for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index] - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName - const include = _.get(modelOpts, 'include', []) + const modelOpts = dataModels[index]; + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; + const include = _.get(modelOpts, 'include', []); - currentModelName = modelName - const model = models[modelName] - const modelRecords = jsonData[modelName] + currentModelName = modelName; + const model = models[modelName]; + const modelRecords = jsonData[modelName]; if (modelRecords && modelRecords.length > 0) { - logger.info({ component: 'importData', message: `Importing data for model: ${modelName}` }) - - await model.bulkCreate(modelRecords, { include, transaction }) - logger.info({ component: 'importData', message: `Records imported for model: ${modelName} = ${modelRecords.length}` }) + logger.info({ + component: 'importData', + message: `Importing data for model: ${modelName}`, + }); + + await model.bulkCreate(modelRecords, { include, transaction }); + logger.info({ + component: 'importData', + message: `Records imported for model: ${modelName} = ${modelRecords.length}`, + }); } else { - logger.info({ component: 'importData', message: `No records to import for model: ${modelName}` }) + logger.info({ + component: 'importData', + message: `No records to import for model: ${modelName}`, + }); } } // commit transaction only if all things went ok - logger.info({ component: 'importData', message: 'committing transaction to database...' }) - await transaction.commit() + logger.info({ + component: 'importData', + message: 'committing transaction to database...', + }); + await transaction.commit(); } catch (error) { - logger.error({ component: 'importData', message: `Error while writing data of model: ${currentModelName}` }) + logger.error({ + component: 'importData', + message: `Error while writing data of model: ${currentModelName}`, + }); // rollback all insert operations if (transaction) { - logger.info({ component: 'importData', message: 'rollback database transaction...' }) - transaction.rollback() + logger.info({ + component: 'importData', + message: 'rollback database transaction...', + }); + transaction.rollback(); } if (error.name && error.errors && error.fields) { // For sequelize validation errors, we throw only fields with data that helps in debugging error, @@ -418,36 +519,50 @@ async function importData (pathToFile, dataModels, logger) { modelName: currentModelName, name: error.name, errors: error.errors, - fields: error.fields + fields: error.fields, }) - ) + ); } else { - throw error + throw error; } } // after importing, index data const jobCandidateModelOpts = { modelName: 'JobCandidate', - include: [{ - model: models.Interview, - as: 'interviews' - }] - } + include: [ + { + model: models.Interview, + as: 'interviews', + }, + ], + }; const resourceBookingModelOpts = { modelName: 'ResourceBooking', - include: [{ - model: models.WorkPeriod, - as: 'workPeriods', - include: [{ - model: models.WorkPeriodPayment, - as: 'payments' - }] - }] - } - await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) - await indexBulkDataToES(jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger) - await indexBulkDataToES(resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger) + include: [ + { + model: models.WorkPeriod, + as: 'workPeriods', + include: [ + { + model: models.WorkPeriodPayment, + as: 'payments', + }, + ], + }, + ], + }; + await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger); + await indexBulkDataToES( + jobCandidateModelOpts, + config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), + logger + ); + await indexBulkDataToES( + resourceBookingModelOpts, + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + logger + ); } /** @@ -456,65 +571,74 @@ async function importData (pathToFile, dataModels, logger) { * @param {Array} dataModels the data models to export * @param {Object} logger the logger object */ -async function exportData (pathToFile, dataModels, logger) { - logger.info({ component: 'exportData', message: `Start Saving data to file with path ${pathToFile}....` }) +async function exportData(pathToFile, dataModels, logger) { + logger.info({ + component: 'exportData', + message: `Start Saving data to file with path ${pathToFile}....`, + }); - const allModelsRecords = {} + const allModelsRecords = {}; for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index] - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName - const include = _.get(modelOpts, 'include', []) - const modelRecords = await models[modelName].findAll({ include }) - const rawRecords = _.map(modelRecords, r => r.toJSON()) - allModelsRecords[modelName] = rawRecords - logger.info({ component: 'exportData', message: `Records loaded for model: ${modelName} = ${rawRecords.length}` }) + const modelOpts = dataModels[index]; + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; + const include = _.get(modelOpts, 'include', []); + const modelRecords = await models[modelName].findAll({ include }); + const rawRecords = _.map(modelRecords, (r) => r.toJSON()); + allModelsRecords[modelName] = rawRecords; + logger.info({ + component: 'exportData', + message: `Records loaded for model: ${modelName} = ${rawRecords.length}`, + }); } - fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)) - logger.info({ component: 'exportData', message: 'End Saving data to file....' }) + fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)); + logger.info({ + component: 'exportData', + message: 'End Saving data to file....', + }); } /** * Format a time in milliseconds into a human readable format * @param {Date} milliseconds the number of milliseconds */ -function formatTime (millisec) { - const ms = Math.floor(millisec % 1000) - const secs = Math.floor((millisec / 1000) % 60) - const mins = Math.floor((millisec / (1000 * 60)) % 60) - const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24) - const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7) - const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4) - const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12) - const yrs = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12))) - - let formattedTime = '0 milliseconds' +function formatTime(millisec) { + const ms = Math.floor(millisec % 1000); + const secs = Math.floor((millisec / 1000) % 60); + const mins = Math.floor((millisec / (1000 * 60)) % 60); + const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24); + const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7); + const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4); + const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12); + const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)); + + let formattedTime = '0 milliseconds'; if (ms > 0) { - formattedTime = `${ms} milliseconds` + formattedTime = `${ms} milliseconds`; } if (secs > 0) { - formattedTime = `${secs} seconds ${formattedTime}` + formattedTime = `${secs} seconds ${formattedTime}`; } if (mins > 0) { - formattedTime = `${mins} minutes ${formattedTime}` + formattedTime = `${mins} minutes ${formattedTime}`; } if (hrs > 0) { - formattedTime = `${hrs} hours ${formattedTime}` + formattedTime = `${hrs} hours ${formattedTime}`; } if (days > 0) { - formattedTime = `${days} days ${formattedTime}` + formattedTime = `${days} days ${formattedTime}`; } if (weeks > 0) { - formattedTime = `${weeks} weeks ${formattedTime}` + formattedTime = `${weeks} weeks ${formattedTime}`; } if (mnths > 0) { - formattedTime = `${mnths} months ${formattedTime}` + formattedTime = `${mnths} months ${formattedTime}`; } if (yrs > 0) { - formattedTime = `${yrs} years ${formattedTime}` + formattedTime = `${yrs} years ${formattedTime}`; } - return formattedTime.trim() + return formattedTime.trim(); } /** @@ -523,30 +647,30 @@ function formatTime (millisec) { * @param {Array} source the array in which to search for the term * @param {Array | String} term the term to search */ -function checkIfExists (source, term) { - let terms +function checkIfExists(source, term) { + let terms; if (!_.isArray(source)) { - throw new Error('Source argument should be an array') + throw new Error('Source argument should be an array'); } - source = source.map(s => s.toLowerCase()) + source = source.map((s) => s.toLowerCase()); if (_.isString(term)) { - terms = term.toLowerCase().split(' ') + terms = term.toLowerCase().split(' '); } else if (_.isArray(term)) { - terms = term.map(t => t.toLowerCase()) + terms = term.map((t) => t.toLowerCase()); } else { - throw new Error('Term argument should be either a string or an array') + throw new Error('Term argument should be either a string or an array'); } for (let i = 0; i < terms.length; i++) { if (source.includes(terms[i])) { - return true + return true; } } - return false + return false; } /** @@ -554,10 +678,10 @@ function checkIfExists (source, term) { * @param {Function} fn the async function * @returns {Function} the wrapped function */ -function wrapExpress (fn) { +function wrapExpress(fn) { return function (req, res, next) { - fn(req, res, next).catch(next) - } + fn(req, res, next).catch(next); + }; } /** @@ -565,20 +689,20 @@ function wrapExpress (fn) { * @param obj the object (controller exports) * @returns {Object|Array} the wrapped object */ -function autoWrapExpress (obj) { +function autoWrapExpress(obj) { if (_.isArray(obj)) { - return obj.map(autoWrapExpress) + return obj.map(autoWrapExpress); } if (_.isFunction(obj)) { if (obj.constructor.name === 'AsyncFunction') { - return wrapExpress(obj) + return wrapExpress(obj); } - return obj + return obj; } _.each(obj, (value, key) => { - obj[key] = autoWrapExpress(value) - }) - return obj + obj[key] = autoWrapExpress(value); + }); + return obj; } /** @@ -587,9 +711,11 @@ function autoWrapExpress (obj) { * @param {Number} page the page number * @returns {String} link for the page */ -function getPageLink (req, page) { - const q = _.assignIn({}, req.query, { page }) - return `${req.protocol}://${req.get('Host')}${req.baseUrl}${req.path}?${querystring.stringify(q)}` +function getPageLink(req, page) { + const q = _.assignIn({}, req.query, { page }); + return `${req.protocol}://${req.get('Host')}${req.baseUrl}${ + req.path + }?${querystring.stringify(q)}`; } /** @@ -598,31 +724,34 @@ function getPageLink (req, page) { * @param {Object} res the HTTP response * @param {Object} result the operation result */ -function setResHeaders (req, res, result) { +function setResHeaders(req, res, result) { if (result.fromDb) { - return + return; } - const totalPages = Math.ceil(result.total / result.perPage) + const totalPages = Math.ceil(result.total / result.perPage); if (result.page > 1) { - res.set('X-Prev-Page', result.page - 1) + res.set('X-Prev-Page', result.page - 1); } if (result.page < totalPages) { - res.set('X-Next-Page', result.page + 1) + res.set('X-Next-Page', result.page + 1); } - res.set('X-Page', result.page) - res.set('X-Per-Page', result.perPage) - res.set('X-Total', result.total) - res.set('X-Total-Pages', totalPages) + res.set('X-Page', result.page); + res.set('X-Per-Page', result.perPage); + res.set('X-Total', result.total); + res.set('X-Total-Pages', totalPages); // set Link header if (totalPages > 0) { - let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink(req, totalPages)}>; rel="last"` + let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink( + req, + totalPages + )}>; rel="last"`; if (result.page > 1) { - link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"` + link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"`; } if (result.page < totalPages) { - link += `, <${getPageLink(req, result.page + 1)}>; rel="next"` + link += `, <${getPageLink(req, result.page + 1)}>; rel="next"`; } - res.set('Link', link) + res.set('Link', link); } } @@ -630,30 +759,30 @@ function setResHeaders (req, res, result) { * Get ES Client * @return {Object} Elastic Host Client Instance */ -function getESClient () { +function getESClient() { if (esClients.client) { - return esClients.client + return esClients.client; } - const host = config.esConfig.HOST - const cloudId = config.esConfig.ELASTICCLOUD.id + const host = config.esConfig.HOST; + const cloudId = config.esConfig.ELASTICCLOUD.id; if (cloudId) { // Elastic Cloud configuration esClients.client = new elasticsearch.Client({ cloud: { - id: cloudId + id: cloudId, }, auth: { username: config.esConfig.ELASTICCLOUD.username, - password: config.esConfig.ELASTICCLOUD.password - } - }) + password: config.esConfig.ELASTICCLOUD.password, + }, + }); } else { esClients.client = new elasticsearch.Client({ - node: host - }) + node: host, + }); } - return esClients.client + return esClients.client; } /* @@ -661,16 +790,22 @@ function getESClient () { * @returns {Promise} */ const getM2MToken = async () => { - return await m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) -} + return await m2m.getMachineToken( + config.AUTH0_CLIENT_ID, + config.AUTH0_CLIENT_SECRET + ); +}; /* * Function to get M2M token for U-Bahn * @returns {Promise} */ const getM2MUbahnToken = async () => { - return await m2mForUbahn.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) -} + return await m2mForUbahn.getMachineToken( + config.AUTH0_CLIENT_ID, + config.AUTH0_CLIENT_SECRET + ); +}; /** * Function to encode query string @@ -678,17 +813,17 @@ const getM2MUbahnToken = async () => { * @param {String} nesting the nesting string * @returns {String} query string */ -function encodeQueryString (queryObj, nesting = '') { +function encodeQueryString(queryObj, nesting = '') { const pairs = Object.entries(queryObj).map(([key, val]) => { // Handle the nested, recursive case, where the value to encode is an object itself if (typeof val === 'object') { - return encodeQueryString(val, nesting + `${key}.`) + return encodeQueryString(val, nesting + `${key}.`); } else { // Handle base case, where the value to encode is simply a string. - return [nesting + key, val].map(querystring.escape).join('=') + return [nesting + key, val].map(querystring.escape).join('='); } - }) - return pairs.join('&') + }); + return pairs.join('&'); } /** @@ -696,28 +831,31 @@ function encodeQueryString (queryObj, nesting = '') { * @param {Integer} externalId the legacy user id * @returns {Array} the users found */ -async function listUsersByExternalId (externalId) { +async function listUsersByExternalId(externalId) { // return empty list if externalId is null or undefined if (!!externalId !== true) { - return [] + return []; } - const token = await getM2MUbahnToken() + const token = await getM2MUbahnToken(); const q = { enrich: true, externalProfile: { organizationId: config.ORG_ID, - externalId - } - } - const url = `${config.TC_API}/users?${encodeQueryString(q)}` + externalId, + }, + }; + const url = `${config.TC_API}/users?${encodeQueryString(q)}`; const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'listUserByExternalId', message: `response body: ${JSON.stringify(res.body)}` }) - return res.body + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'listUserByExternalId', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return res.body; } /** @@ -725,12 +863,14 @@ async function listUsersByExternalId (externalId) { * @param {Integer} externalId the legacy user id * @returns {Object} the user */ -async function getUserByExternalId (externalId) { - const users = await listUsersByExternalId(externalId) +async function getUserByExternalId(externalId) { + const users = await listUsersByExternalId(externalId); if (_.isEmpty(users)) { - throw new errors.NotFoundError(`externalId: ${externalId} "user" not found`) + throw new errors.NotFoundError( + `externalId: ${externalId} "user" not found` + ); } - return users[0] + return users[0]; } /** @@ -739,18 +879,24 @@ async function getUserByExternalId (externalId) { * @params {Object} payload the payload * @params {Object} options the extra options to control the function */ -async function postEvent (topic, payload, options = {}) { - logger.debug({ component: 'helper', context: 'postEvent', message: `Posting event to Kafka topic ${topic}, ${JSON.stringify(payload)}` }) - const client = getBusApiClient() +async function postEvent(topic, payload, options = {}) { + logger.debug({ + component: 'helper', + context: 'postEvent', + message: `Posting event to Kafka topic ${topic}, ${JSON.stringify( + payload + )}`, + }); + const client = getBusApiClient(); const message = { topic, originator: config.KAFKA_MESSAGE_ORIGINATOR, timestamp: new Date().toISOString(), 'mime-type': 'application/json', - payload - } - await client.postEvent(message) - await eventDispatcher.handleEvent(topic, { value: payload, options }) + payload, + }; + await client.postEvent(message); + await eventDispatcher.handleEvent(topic, { value: payload, options }); } /** @@ -759,11 +905,11 @@ async function postEvent (topic, payload, options = {}) { * @param {Object} err the err * @returns {Boolean} the result */ -function isDocumentMissingException (err) { +function isDocumentMissingException(err) { if (err.statusCode === 404 && err instanceof ESResponseError) { - return true + return true; } - return false + return false; } /** @@ -772,31 +918,34 @@ function isDocumentMissingException (err) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getProjects (currentUser, criteria = {}) { - let token +async function getProjects(currentUser, criteria = {}) { + let token; if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken() - token = `Bearer ${m2mToken}` + const m2mToken = await getM2MToken(); + token = `Bearer ${m2mToken}`; } else { - token = currentUser.jwtToken + token = currentUser.jwtToken; } - const url = `${config.TC_API}/projects?type=talent-as-a-service` + const url = `${config.TC_API}/projects?type=talent-as-a-service`; const res = await request .get(url) .query(criteria) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'getProjects', message: `response body: ${JSON.stringify(res.body)}` }) - const result = _.map(res.body, item => { - return _.pick(item, ['id', 'name', 'invites', 'members']) - }) + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getProjects', + message: `response body: ${JSON.stringify(res.body)}`, + }); + const result = _.map(res.body, (item) => { + return _.pick(item, ['id', 'name', 'invites', 'members']); + }); return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result - } + result, + }; } /** @@ -805,19 +954,24 @@ async function getProjects (currentUser, criteria = {}) { * @param {String} userId the legacy user id * @returns {Object} the user */ -async function getTopcoderUserById (userId) { - const token = await getM2MToken() +async function getTopcoderUserById(userId) { + const token = await getM2MToken(); const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `id=${userId}` }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json') - localLogger.debug({ context: 'getTopcoderUserById', message: `response body: ${JSON.stringify(res.body)}` }) - const user = _.get(res.body, 'result.content[0]') + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getTopcoderUserById', + message: `response body: ${JSON.stringify(res.body)}`, + }); + const user = _.get(res.body, 'result.content[0]'); if (!user) { - throw new errors.NotFoundError(`userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}`) + throw new errors.NotFoundError( + `userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}` + ); } - return user + return user; } /** @@ -825,24 +979,31 @@ async function getTopcoderUserById (userId) { * @param {String} userId the user id * @returns the request result */ -async function getUserById (userId, enrich) { - const token = await getM2MUbahnToken() +async function getUserById(userId, enrich) { + const token = await getM2MUbahnToken(); const res = await request .get(`${config.TC_API}/users/${userId}` + (enrich ? '?enrich=true' : '')) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'getUserById', message: `response body: ${JSON.stringify(res.body)}` }) + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getUserById', + message: `response body: ${JSON.stringify(res.body)}`, + }); - const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']) + const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']); if (enrich) { - user.skills = (res.body.skills || []).map((skillObj) => _.pick(skillObj.skill, ['id', 'name'])) - const attributes = _.get(res, 'body.attributes', []) - user.attributes = _.map(attributes, attr => _.pick(attr, ['id', 'value', 'attribute.id', 'attribute.name'])) + user.skills = (res.body.skills || []).map((skillObj) => + _.pick(skillObj.skill, ['id', 'name']) + ); + const attributes = _.get(res, 'body.attributes', []); + user.attributes = _.map(attributes, (attr) => + _.pick(attr, ['id', 'value', 'attribute.id', 'attribute.name']) + ); } - return user + return user; } /** @@ -850,16 +1011,19 @@ async function getUserById (userId, enrich) { * @param {Object} data the user data * @returns the request result */ -async function createUbahnUser ({ handle, firstName, lastName }) { - const token = await getM2MUbahnToken() +async function createUbahnUser({ handle, firstName, lastName }) { + const token = await getM2MUbahnToken(); const res = await request .post(`${config.TC_API}/users`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ handle, firstName, lastName }) - localLogger.debug({ context: 'createUbahnUser', message: `response body: ${JSON.stringify(res.body)}` }) - return _.pick(res.body, ['id']) + .send({ handle, firstName, lastName }); + localLogger.debug({ + context: 'createUbahnUser', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.pick(res.body, ['id']); } /** @@ -867,15 +1031,21 @@ async function createUbahnUser ({ handle, firstName, lastName }) { * @param {String} userId the user id(with uuid format) * @param {Object} data the profile data */ -async function createUserExternalProfile (userId, { organizationId, externalId }) { - const token = await getM2MUbahnToken() +async function createUserExternalProfile( + userId, + { organizationId, externalId } +) { + const token = await getM2MUbahnToken(); const res = await request .post(`${config.TC_API}/users/${userId}/externalProfiles`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ organizationId, externalId: String(externalId) }) - localLogger.debug({ context: 'createUserExternalProfile', message: `response body: ${JSON.stringify(res.body)}` }) + .send({ organizationId, externalId: String(externalId) }); + localLogger.debug({ + context: 'createUserExternalProfile', + message: `response body: ${JSON.stringify(res.body)}`, + }); } /** @@ -883,20 +1053,23 @@ async function createUserExternalProfile (userId, { organizationId, externalId } * @param {Array} handles the handle array * @returns the request result */ -async function getMembers (handles) { - const token = await getM2MToken() - const handlesStr = _.map(handles, handle => { - return '%22' + handle.toLowerCase() + '%22' - }).join(',') - const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]` +async function getMembers(handles) { + const token = await getM2MToken(); + const handlesStr = _.map(handles, (handle) => { + return '%22' + handle.toLowerCase() + '%22'; + }).join(','); + const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]`; const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'getMembers', message: `response body: ${JSON.stringify(res.body)}` }) - return res.body + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getMembers', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return res.body; } /** @@ -905,31 +1078,36 @@ async function getMembers (handles) { * @param {Number} id project id * @returns the request result */ -async function getProjectById (currentUser, id) { - let token +async function getProjectById(currentUser, id) { + let token; if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken() - token = `Bearer ${m2mToken}` + const m2mToken = await getM2MToken(); + token = `Bearer ${m2mToken}`; } else { - token = currentUser.jwtToken + token = currentUser.jwtToken; } - const url = `${config.TC_API}/projects/${id}` + const url = `${config.TC_API}/projects/${id}`; try { const res = await request .get(url) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'getProjectById', message: `response body: ${JSON.stringify(res.body)}` }) - return _.pick(res.body, ['id', 'name', 'invites', 'members']) + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getProjectById', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.pick(res.body, ['id', 'name', 'invites', 'members']); } catch (err) { if (err.status === HttpStatus.FORBIDDEN) { - throw new errors.ForbiddenError(`You are not allowed to access the project with id ${id}`) + throw new errors.ForbiddenError( + `You are not allowed to access the project with id ${id}` + ); } if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${id} project not found`) + throw new errors.NotFoundError(`id: ${id} project not found`); } - throw err + throw err; } } @@ -940,30 +1118,33 @@ async function getProjectById (currentUser, id) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getTopcoderSkills (criteria) { - const token = await getM2MUbahnToken() +async function getTopcoderSkills(criteria) { + const token = await getM2MUbahnToken(); try { const res = await request .get(`${config.TC_API}/skills`) .query({ skillProviderId: config.TOPCODER_SKILL_PROVIDER_ID, - ...criteria + ...criteria, }) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'getTopcoderSkills', message: `response body: ${JSON.stringify(res.body)}` }) + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getTopcoderSkills', + message: `response body: ${JSON.stringify(res.body)}`, + }); return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result: res.body - } + result: res.body, + }; } catch (err) { if (err.status === HttpStatus.BAD_REQUEST) { - throw new errors.BadRequestError(err.response.body.message) + throw new errors.BadRequestError(err.response.body.message); } - throw err + throw err; } } @@ -972,15 +1153,18 @@ async function getTopcoderSkills (criteria) { * @param {String} skillId the skill Id * @returns the request result */ -async function getSkillById (skillId) { - const token = await getM2MUbahnToken() +async function getSkillById(skillId) { + const token = await getM2MUbahnToken(); const res = await request .get(`${config.TC_API}/skills/${skillId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'getSkillById', message: `response body: ${JSON.stringify(res.body)}` }) - return _.pick(res.body, ['id', 'name']) + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getSkillById', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.pick(res.body, ['id', 'name']); } /** @@ -993,17 +1177,22 @@ async function getSkillById (skillId) { * @params {Object} currentUser the user who perform this operation * @returns {String} the ubahn user id */ -async function ensureUbahnUserId (currentUser) { +async function ensureUbahnUserId(currentUser) { try { - return (await getUserByExternalId(currentUser.userId)).id + return (await getUserByExternalId(currentUser.userId)).id; } catch (err) { if (!(err instanceof errors.NotFoundError)) { - throw err + throw err; } - const topcoderUser = await getTopcoderUserById(currentUser.userId) - const user = await createUbahnUser(_.pick(topcoderUser, ['handle', 'firstName', 'lastName'])) - await createUserExternalProfile(user.id, { organizationId: config.ORG_ID, externalId: currentUser.userId }) - return user.id + const topcoderUser = await getTopcoderUserById(currentUser.userId); + const user = await createUbahnUser( + _.pick(topcoderUser, ['handle', 'firstName', 'lastName']) + ); + await createUserExternalProfile(user.id, { + organizationId: config.ORG_ID, + externalId: currentUser.userId, + }); + return user.id; } } @@ -1013,8 +1202,8 @@ async function ensureUbahnUserId (currentUser) { * @param {String} jobId the job id * @returns {Object} the job data */ -async function ensureJobById (jobId) { - return models.Job.findById(jobId) +async function ensureJobById(jobId) { + return models.Job.findById(jobId); } /** @@ -1023,8 +1212,8 @@ async function ensureJobById (jobId) { * @param {String} resourceBookingId the resourceBooking id * @returns {Object} the resourceBooking data */ -async function ensureResourceBookingById (resourceBookingId) { - return models.ResourceBooking.findById(resourceBookingId) +async function ensureResourceBookingById(resourceBookingId) { + return models.ResourceBooking.findById(resourceBookingId); } /** @@ -1032,8 +1221,8 @@ async function ensureResourceBookingById (resourceBookingId) { * @param {String} workPeriodId the workPeriod id * @returns the workPeriod data */ -async function ensureWorkPeriodById (workPeriodId) { - return models.WorkPeriod.findById(workPeriodId) +async function ensureWorkPeriodById(workPeriodId) { + return models.WorkPeriod.findById(workPeriodId); } /** @@ -1042,21 +1231,24 @@ async function ensureWorkPeriodById (workPeriodId) { * @param {String} jobId the user id * @returns {Object} the user data */ -async function ensureUserById (userId) { - const token = await getM2MUbahnToken() +async function ensureUserById(userId) { + const token = await getM2MUbahnToken(); try { const res = await request .get(`${config.TC_API}/users/${userId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'ensureUserById', message: `response body: ${JSON.stringify(res.body)}` }) - return res.body + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'ensureUserById', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return res.body; } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${userId} "user" not found`) + throw new errors.NotFoundError(`id: ${userId} "user" not found`); } - throw err + throw err; } } @@ -1065,8 +1257,12 @@ async function ensureUserById (userId) { * * @returns {Object} the M2M auth user */ -function getAuditM2Muser () { - return { isMachine: true, userId: config.m2m.M2M_AUDIT_USER_ID, handle: config.m2m.M2M_AUDIT_HANDLE } +function getAuditM2Muser() { + return { + isMachine: true, + userId: config.m2m.M2M_AUDIT_USER_ID, + handle: config.m2m.M2M_AUDIT_HANDLE, + }; } /** @@ -1078,17 +1274,24 @@ function getAuditM2Muser () { * @param {Number} projectId project id * @returns the result */ -async function checkIsMemberOfProject (userId, projectId) { - const m2mToken = await getM2MToken() +async function checkIsMemberOfProject(userId, projectId) { + const m2mToken = await getM2MToken(); const res = await request .get(`${config.TC_API}/projects/${projectId}`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - const memberIdList = _.map(res.body.members, 'userId') - localLogger.debug({ context: 'checkIsMemberOfProject', message: `the members of project ${projectId}: ${JSON.stringify(memberIdList)}, authUserId: ${JSON.stringify(userId)}` }) + .set('Accept', 'application/json'); + const memberIdList = _.map(res.body.members, 'userId'); + localLogger.debug({ + context: 'checkIsMemberOfProject', + message: `the members of project ${projectId}: ${JSON.stringify( + memberIdList + )}, authUserId: ${JSON.stringify(userId)}`, + }); if (!memberIdList.includes(userId)) { - throw new errors.UnauthorizedError(`userId: ${userId} the user is not a member of project ${projectId}`) + throw new errors.UnauthorizedError( + `userId: ${userId} the user is not a member of project ${projectId}` + ); } } @@ -1098,21 +1301,27 @@ async function checkIsMemberOfProject (userId, projectId) { * @param {Array} handles the array of handles * @returns {Array} the member details */ -async function getMemberDetailsByHandles (handles) { +async function getMemberDetailsByHandles(handles) { if (!handles.length) { - return [] + return []; } - const token = await getM2MToken() + const token = await getM2MToken(); const res = await request .get(`${config.TOPCODER_MEMBERS_API}/_search`) .query({ - query: _.map(handles, handle => `handleLower:${handle.toLowerCase()}`).join(' OR '), - fields: 'userId,handle,firstName,lastName,email' + query: _.map( + handles, + (handle) => `handleLower:${handle.toLowerCase()}` + ).join(' OR '), + fields: 'userId,handle,firstName,lastName,email', }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json') - localLogger.debug({ context: 'getMemberDetailsByHandles', message: `response body: ${JSON.stringify(res.body)}` }) - return _.get(res.body, 'result.content') + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getMemberDetailsByHandles', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.get(res.body, 'result.content'); } /** @@ -1121,14 +1330,17 @@ async function getMemberDetailsByHandles (handles) { * @param {String} handle the user handle * @returns {Object} the member details */ -async function getV3MemberDetailsByHandle (handle) { - const token = await getM2MToken() +async function getV3MemberDetailsByHandle(handle) { + const token = await getM2MToken(); const res = await request .get(`${config.TOPCODER_MEMBERS_API}/${handle}`) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json') - localLogger.debug({ context: 'getV3MemberDetailsByHandle', message: `response body: ${JSON.stringify(res.body)}` }) - return _.get(res.body, 'result.content') + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getV3MemberDetailsByHandle', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.get(res.body, 'result.content'); } /** @@ -1138,17 +1350,20 @@ async function getV3MemberDetailsByHandle (handle) { * @param {String} email the email * @returns {Array} the member details */ -async function _getMemberDetailsByEmail (token, email) { +async function _getMemberDetailsByEmail(token, email) { const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `email=${email}`, - fields: 'handle,id,email,firstName,lastName' + fields: 'handle,id,email,firstName,lastName', }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json') - localLogger.debug({ context: '_getMemberDetailsByEmail', message: `response body: ${JSON.stringify(res.body)}` }) - return _.get(res.body, 'result.content') + .set('Accept', 'application/json'); + localLogger.debug({ + context: '_getMemberDetailsByEmail', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.get(res.body, 'result.content'); } /** @@ -1158,16 +1373,25 @@ async function _getMemberDetailsByEmail (token, email) { * @param {Array} emails the array of emails * @returns {Array} the member details */ -async function getMemberDetailsByEmails (emails) { - const token = await getM2MToken() - const limiter = new Bottleneck({ maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API }) - const membersArray = await Promise.all(emails.map(email => limiter.schedule(() => _getMemberDetailsByEmail(token, email) - .catch((error) => { - localLogger.error({ context: 'getMemberDetailsByEmails', message: error.message }) - return [] - }) - ))) - return _.flatten(membersArray) +async function getMemberDetailsByEmails(emails) { + const token = await getM2MToken(); + const limiter = new Bottleneck({ + maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API, + }); + const membersArray = await Promise.all( + emails.map((email) => + limiter.schedule(() => + _getMemberDetailsByEmail(token, email).catch((error) => { + localLogger.error({ + context: 'getMemberDetailsByEmails', + message: error.message, + }); + return []; + }) + ) + ) + ); + return _.flatten(membersArray); } /** @@ -1178,17 +1402,20 @@ async function getMemberDetailsByEmails (emails) { * @param {Object} criteria the filtering criteria * @returns {Object} the member created */ -async function createProjectMember (projectId, data, criteria) { - const m2mToken = await getM2MToken() +async function createProjectMember(projectId, data, criteria) { + const m2mToken = await getM2MToken(); const { body: member } = await request .post(`${config.TC_API}/projects/${projectId}/members`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') .query(criteria) - .send(data) - localLogger.debug({ context: 'createProjectMember', message: `response body: ${JSON.stringify(member)}` }) - return member + .send(data); + localLogger.debug({ + context: 'createProjectMember', + message: `response body: ${JSON.stringify(member)}`, + }); + return member; } /** @@ -1198,17 +1425,21 @@ async function createProjectMember (projectId, data, criteria) { * @param {Object} criteria the search criteria * @returns {Array} the project members */ -async function listProjectMembers (currentUser, projectId, criteria = {}) { - const token = (currentUser.hasManagePermission || currentUser.isMachine) - ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken +async function listProjectMembers(currentUser, projectId, criteria = {}) { + const token = + currentUser.hasManagePermission || currentUser.isMachine + ? `Bearer ${await getM2MToken()}` + : currentUser.jwtToken; const { body: members } = await request .get(`${config.TC_API}/projects/${projectId}/members`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json') - localLogger.debug({ context: 'listProjectMembers', message: `response body: ${JSON.stringify(members)}` }) - return members + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'listProjectMembers', + message: `response body: ${JSON.stringify(members)}`, + }); + return members; } /** @@ -1218,17 +1449,21 @@ async function listProjectMembers (currentUser, projectId, criteria = {}) { * @param {Object} criteria the search criteria * @returns {Array} the member invites */ -async function listProjectMemberInvites (currentUser, projectId, criteria = {}) { - const token = (currentUser.hasManagePermission || currentUser.isMachine) - ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken +async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { + const token = + currentUser.hasManagePermission || currentUser.isMachine + ? `Bearer ${await getM2MToken()}` + : currentUser.jwtToken; const { body: invites } = await request .get(`${config.TC_API}/projects/${projectId}/invites`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json') - localLogger.debug({ context: 'listProjectMemberInvites', message: `response body: ${JSON.stringify(invites)}` }) - return invites + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'listProjectMemberInvites', + message: `response body: ${JSON.stringify(invites)}`, + }); + return invites; } /** @@ -1238,19 +1473,24 @@ async function listProjectMemberInvites (currentUser, projectId, criteria = {}) * @param {String} projectMemberId the id of the project member * @returns {undefined} */ -async function deleteProjectMember (currentUser, projectId, projectMemberId) { - const token = (currentUser.hasManagePermission || currentUser.isMachine) - ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken +async function deleteProjectMember(currentUser, projectId, projectMemberId) { + const token = + currentUser.hasManagePermission || currentUser.isMachine + ? `Bearer ${await getM2MToken()}` + : currentUser.jwtToken; try { await request - .delete(`${config.TC_API}/projects/${projectId}/members/${projectMemberId}`) - .set('Authorization', token) + .delete( + `${config.TC_API}/projects/${projectId}/members/${projectMemberId}` + ) + .set('Authorization', token); } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}`) + throw new errors.NotFoundError( + `projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}` + ); } - throw err + throw err; } } @@ -1260,10 +1500,13 @@ async function deleteProjectMember (currentUser, projectId, projectMemberId) { * @param {String} attributeName Requested attribute name, e.g. "email" * @returns attribute value */ -function getUserAttributeValue (user, attributeName) { - const attributes = _.get(user, 'attributes', []) - const targetAttribute = _.find(attributes, a => a.attribute.name === attributeName) - return _.get(targetAttribute, 'value') +function getUserAttributeValue(user, attributeName) { + const attributes = _.get(user, 'attributes', []); + const targetAttribute = _.find( + attributes, + (a) => a.attribute.name === attributeName + ); + return _.get(targetAttribute, 'value'); } /** @@ -1273,22 +1516,34 @@ function getUserAttributeValue (user, attributeName) { * @param {String} token m2m token * @returns {Object} the challenge created */ -async function createChallenge (data, token) { +async function createChallenge(data, token) { if (!token) { - token = await getM2MToken() + token = await getM2MToken(); } - const url = `${config.TC_API}/challenges` - localLogger.debug({ context: 'createChallenge', message: `EndPoint: POST ${url}` }) - localLogger.debug({ context: 'createChallenge', message: `Request Body: ${JSON.stringify(data)}` }) + const url = `${config.TC_API}/challenges`; + localLogger.debug({ + context: 'createChallenge', + message: `EndPoint: POST ${url}`, + }); + localLogger.debug({ + context: 'createChallenge', + message: `Request Body: ${JSON.stringify(data)}`, + }); const { body: challenge, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data) - localLogger.debug({ context: 'createChallenge', message: `Status Code: ${httpStatus}` }) - localLogger.debug({ context: 'createChallenge', message: `Response Body: ${JSON.stringify(challenge)}` }) - return challenge + .send(data); + localLogger.debug({ + context: 'createChallenge', + message: `Status Code: ${httpStatus}`, + }); + localLogger.debug({ + context: 'createChallenge', + message: `Response Body: ${JSON.stringify(challenge)}`, + }); + return challenge; } /** @@ -1299,22 +1554,34 @@ async function createChallenge (data, token) { * @param {String} token m2m token * @returns {Object} the challenge updated */ -async function updateChallenge (challengeId, data, token) { +async function updateChallenge(challengeId, data, token) { if (!token) { - token = await getM2MToken() + token = await getM2MToken(); } - const url = `${config.TC_API}/challenges/${challengeId}` - localLogger.debug({ context: 'updateChallenge', message: `EndPoint: PATCH ${url}` }) - localLogger.debug({ context: 'updateChallenge', message: `Request Body: ${JSON.stringify(data)}` }) + const url = `${config.TC_API}/challenges/${challengeId}`; + localLogger.debug({ + context: 'updateChallenge', + message: `EndPoint: PATCH ${url}`, + }); + localLogger.debug({ + context: 'updateChallenge', + message: `Request Body: ${JSON.stringify(data)}`, + }); const { body: challenge, status: httpStatus } = await request .patch(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data) - localLogger.debug({ context: 'updateChallenge', message: `Status Code: ${httpStatus}` }) - localLogger.debug({ context: 'updateChallenge', message: `Response Body: ${JSON.stringify(challenge)}` }) - return challenge + .send(data); + localLogger.debug({ + context: 'updateChallenge', + message: `Status Code: ${httpStatus}`, + }); + localLogger.debug({ + context: 'updateChallenge', + message: `Response Body: ${JSON.stringify(challenge)}`, + }); + return challenge; } /** @@ -1324,22 +1591,34 @@ async function updateChallenge (challengeId, data, token) { * @param {String} token m2m token * @returns {Object} the resource created */ -async function createChallengeResource (data, token) { +async function createChallengeResource(data, token) { if (!token) { - token = await getM2MToken() + token = await getM2MToken(); } - const url = `${config.TC_API}/resources` - localLogger.debug({ context: 'createChallengeResource', message: `EndPoint: POST ${url}` }) - localLogger.debug({ context: 'createChallengeResource', message: `Request Body: ${JSON.stringify(data)}` }) + const url = `${config.TC_API}/resources`; + localLogger.debug({ + context: 'createChallengeResource', + message: `EndPoint: POST ${url}`, + }); + localLogger.debug({ + context: 'createChallengeResource', + message: `Request Body: ${JSON.stringify(data)}`, + }); const { body: resource, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data) - localLogger.debug({ context: 'createChallengeResource', message: `Status Code: ${httpStatus}` }) - localLogger.debug({ context: 'createChallengeResource', message: `Response Body: ${JSON.stringify(resource)}` }) - return resource + .send(data); + localLogger.debug({ + context: 'createChallengeResource', + message: `Status Code: ${httpStatus}`, + }); + localLogger.debug({ + context: 'createChallengeResource', + message: `Response Body: ${JSON.stringify(resource)}`, + }); + return resource; } /** @@ -1348,40 +1627,40 @@ async function createChallengeResource (data, token) { * @param {Date} end end date of the resource booking * @returns {Array<{startDate:Date, endDate:Date, daysWorked:number}>} information about workPeriods */ -function extractWorkPeriods (start, end) { +function extractWorkPeriods(start, end) { // calculate maximum possible daysWorked for a week - function getDaysWorked (week) { + function getDaysWorked(week) { if (weeks === 1) { - return Math.min(endDay, 5) - Math.max(startDay, 1) + 1 + return Math.min(endDay, 5) - Math.max(startDay, 1) + 1; } else if (week === 0) { - return Math.min(6 - startDay, 5) - } else if (week === (weeks - 1)) { - return Math.min(endDay, 5) - } else return 5 + return Math.min(6 - startDay, 5); + } else if (week === weeks - 1) { + return Math.min(endDay, 5); + } else return 5; } - const periods = [] + const periods = []; if (_.isNil(start) || _.isNil(end)) { - return periods + return periods; } - const startDate = moment(start) - const startDay = startDate.get('day') - startDate.set('day', 0).startOf('day') + const startDate = moment(start); + const startDay = startDate.get('day'); + startDate.set('day', 0).startOf('day'); - const endDate = moment(end) - const endDay = endDate.get('day') - endDate.set('day', 6).endOf('day') + const endDate = moment(end); + const endDay = endDate.get('day'); + endDate.set('day', 6).endOf('day'); - const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7 + const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7; for (let i = 0; i < weeks; i++) { periods.push({ startDate: startDate.format('YYYY-MM-DD'), endDate: startDate.add(6, 'day').format('YYYY-MM-DD'), - daysWorked: getDaysWorked(i) - }) - startDate.add(1, 'day') + daysWorked: getDaysWorked(i), + }); + startDate.add(1, 'day'); } - return periods + return periods; } /** @@ -1390,16 +1669,19 @@ function extractWorkPeriods (start, end) { * @param {String} userHandle user handle * @returns {String} email address of the user */ -async function getUserByHandle (userHandle) { - const token = await getM2MToken() - const url = `${config.TC_API}/members/${userHandle}` +async function getUserByHandle(userHandle) { + const token = await getM2MToken(); + const url = `${config.TC_API}/members/${userHandle}`; const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'getUserByHandle', message: `response body: ${JSON.stringify(res.body)}` }) - return _.get(res, 'body') + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getUserByHandle', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.get(res, 'body'); } /** @@ -1408,14 +1690,34 @@ async function getUserByHandle (userHandle) { * @param {*} object of json that would be replaced in string * @returns */ -async function substituteStringByObject (string, object) { +async function substituteStringByObject(string, object) { for (var key in object) { if (!Object.prototype.hasOwnProperty.call(object, key)) { - continue + continue; } - string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]) + string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]); } - return string + return string; +} + +/** + * @param {Object} currentUser the user performing the action + * @param {Object} data title of project and any other info + * @returns {Object} the project created + */ +async function createProject(currentUser, data) { + const token = currentUser.jwtToken; + const res = await request + .post(`${config.TC_API}/projects/`) + .set('Authorization', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .send(data); + localLogger.debug({ + context: 'createProject', + message: `response body: ${JSON.stringify(res)}`, + }); + return _.get(res, 'body'); } module.exports = { @@ -1434,9 +1736,9 @@ module.exports = { getUserId: async (userId) => { // check m2m user id if (userId === config.m2m.M2M_AUDIT_USER_ID) { - return config.m2m.M2M_AUDIT_USER_ID + return config.m2m.M2M_AUDIT_USER_ID; } - return ensureUbahnUserId({ userId }) + return ensureUbahnUserId({ userId }); }, getUserByExternalId, getM2MToken, @@ -1469,5 +1771,6 @@ module.exports = { createChallengeResource, extractWorkPeriods, getUserByHandle, - substituteStringByObject -} + substituteStringByObject, + createProject, +}; diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index b8f7c149..ca4f1bca 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -1,19 +1,19 @@ /** * Controller for TaaS teams endpoints */ -const HttpStatus = require('http-status-codes') -const service = require('../services/TeamService') -const helper = require('../common/helper') +const HttpStatus = require('http-status-codes'); +const service = require('../services/TeamService'); +const helper = require('../common/helper'); /** * Search teams * @param req the request * @param res the response */ -async function searchTeams (req, res) { - const result = await service.searchTeams(req.authUser, req.query) - helper.setResHeaders(req, res, result) - res.send(result.result) +async function searchTeams(req, res) { + const result = await service.searchTeams(req.authUser, req.query); + helper.setResHeaders(req, res, result); + res.send(result.result); } /** @@ -21,8 +21,8 @@ async function searchTeams (req, res) { * @param req the request * @param res the response */ -async function getTeam (req, res) { - res.send(await service.getTeam(req.authUser, req.params.id)) +async function getTeam(req, res) { + res.send(await service.getTeam(req.authUser, req.params.id)); } /** @@ -30,8 +30,10 @@ async function getTeam (req, res) { * @param req the request * @param res the response */ -async function getTeamJob (req, res) { - res.send(await service.getTeamJob(req.authUser, req.params.id, req.params.jobId)) +async function getTeamJob(req, res) { + res.send( + await service.getTeamJob(req.authUser, req.params.id, req.params.jobId) + ); } /** @@ -39,9 +41,9 @@ async function getTeamJob (req, res) { * @param req the request * @param res the response */ -async function sendEmail (req, res) { - await service.sendEmail(req.authUser, req.body) - res.status(HttpStatus.NO_CONTENT).end() +async function sendEmail(req, res) { + await service.sendEmail(req.authUser, req.body); + res.status(HttpStatus.NO_CONTENT).end(); } /** @@ -49,8 +51,10 @@ async function sendEmail (req, res) { * @param req the request * @param res the response */ -async function addMembers (req, res) { - res.send(await service.addMembers(req.authUser, req.params.id, req.query, req.body)) +async function addMembers(req, res) { + res.send( + await service.addMembers(req.authUser, req.params.id, req.query, req.body) + ); } /** @@ -58,9 +62,13 @@ async function addMembers (req, res) { * @param req the request * @param res the response */ -async function searchMembers (req, res) { - const result = await service.searchMembers(req.authUser, req.params.id, req.query) - res.send(result.result) +async function searchMembers(req, res) { + const result = await service.searchMembers( + req.authUser, + req.params.id, + req.query + ); + res.send(result.result); } /** @@ -68,9 +76,13 @@ async function searchMembers (req, res) { * @param req the request * @param res the response */ -async function searchInvites (req, res) { - const result = await service.searchInvites(req.authUser, req.params.id, req.query) - res.send(result.result) +async function searchInvites(req, res) { + const result = await service.searchInvites( + req.authUser, + req.params.id, + req.query + ); + res.send(result.result); } /** @@ -78,9 +90,13 @@ async function searchInvites (req, res) { * @param req the request * @param res the response */ -async function deleteMember (req, res) { - await service.deleteMember(req.authUser, req.params.id, req.params.projectMemberId) - res.status(HttpStatus.NO_CONTENT).end() +async function deleteMember(req, res) { + await service.deleteMember( + req.authUser, + req.params.id, + req.params.projectMemberId + ); + res.status(HttpStatus.NO_CONTENT).end(); } /** @@ -88,8 +104,17 @@ async function deleteMember (req, res) { * @param req the request * @param res the response */ -async function getMe (req, res) { - res.send(await service.getMe(req.authUser)) +async function getMe(req, res) { + res.send(await service.getMe(req.authUser)); +} + +/** + * + * @param req the request + * @param res the response + */ +async function createProj(req, res) { + res.send(await service.createProj(req.authUser, req.body)); } module.exports = { @@ -101,5 +126,6 @@ module.exports = { searchMembers, searchInvites, deleteMember, - getMe -} + getMe, + createProj, +}; diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index f5d062c6..9bbe25c6 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -1,7 +1,7 @@ /** * Contains taas team routes */ -const constants = require('../../app-constants') +const constants = require('../../app-constants'); module.exports = { '/taas-teams': { @@ -9,77 +9,85 @@ module.exports = { controller: 'TeamController', method: 'searchTeams', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/email': { post: { controller: 'TeamController', method: 'sendEmail', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/skills': { get: { controller: 'SkillController', method: 'searchSkills', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/me': { get: { controller: 'TeamController', method: 'getMe', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/:id': { get: { controller: 'TeamController', method: 'getTeam', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/:id/jobs/:jobId': { get: { controller: 'TeamController', method: 'getTeamJob', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/:id/members': { post: { controller: 'TeamController', method: 'addMembers', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] + scopes: [constants.Scopes.READ_TAAS_TEAM], }, get: { controller: 'TeamController', method: 'searchMembers', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/:id/invites': { get: { controller: 'TeamController', method: 'searchInvites', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/:id/members/:projectMemberId': { delete: { controller: 'TeamController', method: 'deleteMember', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } - } -} + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, + }, + '/taas-teams/createTeamRequest': { + post: { + controller: 'TeamController', + method: 'createProj', + auth: 'jwt', + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, + }, +}; diff --git a/src/services/TeamService.js b/src/services/TeamService.js index a1432fd1..3f6dbfd3 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -2,16 +2,16 @@ * This service provides operations of Job. */ -const _ = require('lodash') -const Joi = require('joi') -const dateFNS = require('date-fns') -const config = require('config') -const emailTemplateConfig = require('../../config/email_template.config') -const helper = require('../common/helper') -const logger = require('../common/logger') -const errors = require('../common/errors') -const JobService = require('./JobService') -const ResourceBookingService = require('./ResourceBookingService') +const _ = require('lodash'); +const Joi = require('joi'); +const dateFNS = require('date-fns'); +const config = require('config'); +const emailTemplateConfig = require('../../config/email_template.config'); +const helper = require('../common/helper'); +const logger = require('../common/logger'); +const errors = require('../common/errors'); +const JobService = require('./JobService'); +const ResourceBookingService = require('./ResourceBookingService'); const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { return { @@ -20,9 +20,9 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { from: template.from, recipients: template.recipients, cc: template.cc, - sendgridTemplateId: template.sendgridTemplateId - } -}) + sendgridTemplateId: template.sendgridTemplateId, + }; +}); /** * Function to get placed resource bookings with specific projectIds @@ -30,10 +30,14 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { * @param {Array} projectIds project ids * @returns the request result */ -async function _getPlacedResourceBookingsByProjectIds (currentUser, projectIds) { - const criteria = { status: 'placed', projectIds } - const { result } = await ResourceBookingService.searchResourceBookings(currentUser, criteria, { returnAll: true }) - return result +async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { + const criteria = { status: 'placed', projectIds }; + const { result } = await ResourceBookingService.searchResourceBookings( + currentUser, + criteria, + { returnAll: true } + ); + return result; } /** @@ -42,9 +46,13 @@ async function _getPlacedResourceBookingsByProjectIds (currentUser, projectIds) * @param {Array} projectIds project ids * @returns the request result */ -async function _getJobsByProjectIds (currentUser, projectIds) { - const { result } = await JobService.searchJobs(currentUser, { projectIds }, { returnAll: true }) - return result +async function _getJobsByProjectIds(currentUser, projectIds) { + const { result } = await JobService.searchJobs( + currentUser, + { projectIds }, + { returnAll: true } + ); + return result; } /** @@ -53,40 +61,59 @@ async function _getJobsByProjectIds (currentUser, projectIds) { * @param {Object} criteria the search criteria * @returns {Object} the search result, contain total/page/perPage and result array */ -async function searchTeams (currentUser, criteria) { - const sort = `${criteria.sortBy} ${criteria.sortOrder}` +async function searchTeams(currentUser, criteria) { + const sort = `${criteria.sortBy} ${criteria.sortOrder}`; // Get projects from /v5/projects with searching criteria - const { total, page, perPage, result: projects } = await helper.getProjects( - currentUser, - { - page: criteria.page, - perPage: criteria.perPage, - name: criteria.name, - sort - } - ) + const { + total, + page, + perPage, + result: projects, + } = await helper.getProjects(currentUser, { + page: criteria.page, + perPage: criteria.perPage, + name: criteria.name, + sort, + }); return { total, page, perPage, - result: await getTeamDetail(currentUser, projects) - } + result: await getTeamDetail(currentUser, projects), + }; } -searchTeams.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - criteria: Joi.object().keys({ - page: Joi.page(), - perPage: Joi.perPage(), - sortBy: Joi.string().valid('createdAt', 'updatedAt', 'lastActivityAt', 'id', 'status', 'name', 'type', 'best match').default('lastActivityAt'), - sortOrder: Joi.when('sortBy', { - is: 'best match', - then: Joi.forbidden().label('sortOrder(with sortBy being `best match`)'), - otherwise: Joi.string().valid('asc', 'desc').default('desc') - }), - name: Joi.string() - }).required() -}).required() +searchTeams.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + criteria: Joi.object() + .keys({ + page: Joi.page(), + perPage: Joi.perPage(), + sortBy: Joi.string() + .valid( + 'createdAt', + 'updatedAt', + 'lastActivityAt', + 'id', + 'status', + 'name', + 'type', + 'best match' + ) + .default('lastActivityAt'), + sortOrder: Joi.when('sortBy', { + is: 'best match', + then: Joi.forbidden().label( + 'sortOrder(with sortBy being `best match`)' + ), + otherwise: Joi.string().valid('asc', 'desc').default('desc'), + }), + name: Joi.string(), + }) + .required(), + }) + .required(); /** * Get team details @@ -95,120 +122,142 @@ searchTeams.schema = Joi.object().keys({ * @param {Object} isSearch the flag whether for search function * @returns {Object} the search result */ -async function getTeamDetail (currentUser, projects, isSearch = true) { - const projectIds = _.map(projects, 'id') +async function getTeamDetail(currentUser, projects, isSearch = true) { + const projectIds = _.map(projects, 'id'); // Get all placed resourceBookings filtered by projectIds - const resourceBookings = await _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) + const resourceBookings = await _getPlacedResourceBookingsByProjectIds( + currentUser, + projectIds + ); // Get all jobs filtered by projectIds - const jobs = await _getJobsByProjectIds(currentUser, projectIds) + const jobs = await _getJobsByProjectIds(currentUser, projectIds); // Get first week day and last week day - const curr = new Date() - const firstDay = dateFNS.startOfWeek(curr) - const lastDay = dateFNS.endOfWeek(curr) + const curr = new Date(); + const firstDay = dateFNS.startOfWeek(curr); + const lastDay = dateFNS.endOfWeek(curr); - logger.debug({ component: 'TeamService', context: 'getTeamDetail', message: `week started: ${firstDay}, week ended: ${lastDay}` }) + logger.debug({ + component: 'TeamService', + context: 'getTeamDetail', + message: `week started: ${firstDay}, week ended: ${lastDay}`, + }); - const result = [] + const result = []; for (const project of projects) { - const rbs = _.filter(resourceBookings, { projectId: project.id }) - const res = _.clone(project) - res.weeklyCost = 0 - res.resources = [] + const rbs = _.filter(resourceBookings, { projectId: project.id }); + const res = _.clone(project); + res.weeklyCost = 0; + res.resources = []; if (rbs && rbs.length > 0) { // Get minimal start date and maximal end date - const startDates = [] - const endDates = [] + const startDates = []; + const endDates = []; for (const rbsItem of rbs) { if (rbsItem.startDate) { - startDates.push(new Date(rbsItem.startDate)) + startDates.push(new Date(rbsItem.startDate)); } if (rbsItem.endDate) { - endDates.push(new Date(rbsItem.endDate)) + endDates.push(new Date(rbsItem.endDate)); } } if (startDates && startDates.length > 0) { - res.startDate = _.min(startDates) + res.startDate = _.min(startDates); } if (endDates && endDates.length > 0) { - res.endDate = _.max(endDates) + res.endDate = _.max(endDates); } // Count weekly rate for (const item of rbs) { // ignore any resourceBooking that has customerRate missed if (!item.customerRate) { - continue + continue; } - const startDate = new Date(item.startDate) - const endDate = new Date(item.endDate) + const startDate = new Date(item.startDate); + const endDate = new Date(item.endDate); // normally startDate is smaller than endDate for a resourceBooking so not check if startDate < endDate - if ((!item.startDate || startDate < lastDay) && - (!item.endDate || endDate > firstDay)) { - res.weeklyCost += item.customerRate + if ( + (!item.startDate || startDate < lastDay) && + (!item.endDate || endDate > firstDay) + ) { + res.weeklyCost += item.customerRate; } } const resourceInfos = await Promise.all( _.map(rbs, (rb) => { - return helper.getUserById(rb.userId, true) - .then(user => { - const resource = { - id: rb.id, - userId: user.id, - ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']) - } - // If call function is not search, add jobId field - if (!isSearch) { - resource.jobId = rb.jobId - resource.customerRate = rb.customerRate - resource.startDate = rb.startDate - resource.endDate = rb.endDate - } - return resource - }) - })) + return helper.getUserById(rb.userId, true).then((user) => { + const resource = { + id: rb.id, + userId: user.id, + ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']), + }; + // If call function is not search, add jobId field + if (!isSearch) { + resource.jobId = rb.jobId; + resource.customerRate = rb.customerRate; + resource.startDate = rb.startDate; + resource.endDate = rb.endDate; + } + return resource; + }); + }) + ); if (resourceInfos && resourceInfos.length > 0) { - res.resources = resourceInfos + res.resources = resourceInfos; - const userHandles = _.map(resourceInfos, 'handle') + const userHandles = _.map(resourceInfos, 'handle'); // Get user photo from /v5/members - const members = await helper.getMembers(userHandles) + const members = await helper.getMembers(userHandles); for (const item of res.resources) { - const findMember = _.find(members, { handleLower: item.handle.toLowerCase() }) + const findMember = _.find(members, { + handleLower: item.handle.toLowerCase(), + }); if (findMember && findMember.photoURL) { - item.photo_url = findMember.photoURL + item.photo_url = findMember.photoURL; } } } } - const jobsTmp = _.filter(jobs, { projectId: project.id }) + const jobsTmp = _.filter(jobs, { projectId: project.id }); if (jobsTmp && jobsTmp.length > 0) { if (isSearch) { // Count total positions - res.totalPositions = 0 + res.totalPositions = 0; for (const item of jobsTmp) { // only sum numPositions of jobs whose status is NOT cancelled or closed if (['cancelled', 'closed'].includes(item.status)) { - continue + continue; } - res.totalPositions += item.numPositions + res.totalPositions += item.numPositions; } } else { - res.jobs = _.map(jobsTmp, job => { - return _.pick(job, ['id', 'description', 'startDate', 'duration', 'numPositions', 'rateType', 'skills', 'customerRate', 'status', 'title']) - }) + res.jobs = _.map(jobsTmp, (job) => { + return _.pick(job, [ + 'id', + 'description', + 'startDate', + 'duration', + 'numPositions', + 'rateType', + 'skills', + 'customerRate', + 'status', + 'title', + ]); + }); } } - result.push(res) + result.push(res); } - return result + return result; } /** @@ -217,31 +266,35 @@ async function getTeamDetail (currentUser, projects, isSearch = true) { * @param {String} id the job id * @returns {Object} the team */ -async function getTeam (currentUser, id) { - const project = await helper.getProjectById(currentUser, id) - const result = await getTeamDetail(currentUser, [project], false) - const teamDetail = result[0] +async function getTeam(currentUser, id) { + const project = await helper.getProjectById(currentUser, id); + const result = await getTeamDetail(currentUser, [project], false); + const teamDetail = result[0]; // add job skills for result - let jobSkills = [] + let jobSkills = []; if (teamDetail && teamDetail.jobs) { for (const job of teamDetail.jobs) { if (job.skills) { - const usersPromises = [] - _.map(job.skills, (skillId) => { usersPromises.push(helper.getSkillById(skillId)) }) - jobSkills = await Promise.all(usersPromises) - job.skills = jobSkills + const usersPromises = []; + _.map(job.skills, (skillId) => { + usersPromises.push(helper.getSkillById(skillId)); + }); + jobSkills = await Promise.all(usersPromises); + job.skills = jobSkills; } } } - return teamDetail + return teamDetail; } -getTeam.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required() -}).required() +getTeam.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + }) + .required(); /** * Get team job with id @@ -250,23 +303,25 @@ getTeam.schema = Joi.object().keys({ * @param {String} jobId the job id * @returns the team job */ -async function getTeamJob (currentUser, id, jobId) { - const project = await helper.getProjectById(currentUser, id) - const jobs = await _getJobsByProjectIds(currentUser, [project.id]) - const job = _.find(jobs, { id: jobId }) +async function getTeamJob(currentUser, id, jobId) { + const project = await helper.getProjectById(currentUser, id); + const jobs = await _getJobsByProjectIds(currentUser, [project.id]); + const job = _.find(jobs, { id: jobId }); if (!job) { - throw new errors.NotFoundError(`id: ${jobId} "Job" with Team id ${id} doesn't exist`) + throw new errors.NotFoundError( + `id: ${jobId} "Job" with Team id ${id} doesn't exist` + ); } const result = { id: job.id, - title: job.title - } + title: job.title, + }; if (job.skills) { result.skills = await Promise.all( _.map(job.skills, (skillId) => helper.getSkillById(skillId)) - ) + ); } // If the job has candidates, the following data for each candidate would be populated: @@ -278,36 +333,49 @@ async function getTeamJob (currentUser, id, jobId) { if (job && job.candidates && job.candidates.length > 0) { // find user data for candidates const users = await Promise.all( - _.map(_.uniq(_.map(job.candidates, 'userId')), userId => helper.getUserById(userId, true)) - ) - const userMap = _.groupBy(users, 'id') + _.map(_.uniq(_.map(job.candidates, 'userId')), (userId) => + helper.getUserById(userId, true) + ) + ); + const userMap = _.groupBy(users, 'id'); // find photo URLs for users - const members = await helper.getMembers(_.map(users, 'handle')) - const photoURLMap = _.groupBy(members, 'handleLower') - - result.candidates = _.map(job.candidates, candidate => { - const candidateData = _.pick(candidate, ['status', 'resume', 'userId', 'interviews', 'id']) - const userData = userMap[candidate.userId][0] + const members = await helper.getMembers(_.map(users, 'handle')); + const photoURLMap = _.groupBy(members, 'handleLower'); + + result.candidates = _.map(job.candidates, (candidate) => { + const candidateData = _.pick(candidate, [ + 'status', + 'resume', + 'userId', + 'interviews', + 'id', + ]); + const userData = userMap[candidate.userId][0]; // attach user data to the candidate - Object.assign(candidateData, _.pick(userData, ['handle', 'firstName', 'lastName', 'skills'])) + Object.assign( + candidateData, + _.pick(userData, ['handle', 'firstName', 'lastName', 'skills']) + ); // attach photo URL to the candidate - const handleLower = userData.handle.toLowerCase() + const handleLower = userData.handle.toLowerCase(); if (photoURLMap[handleLower]) { - candidateData.photo_url = photoURLMap[handleLower][0].photoURL + candidateData.photo_url = photoURLMap[handleLower][0].photoURL; } - return candidateData - }) + return candidateData; + }); } - return result + return result; } -getTeamJob.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required(), - jobId: Joi.string().guid().required() -}).required() +getTeamJob.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + jobId: Joi.string().guid().required(), + }) + .required(); /** * Send email through a particular template @@ -315,18 +383,21 @@ getTeamJob.schema = Joi.object().keys({ * @param {Object} data the email object * @returns {undefined} */ -async function sendEmail (currentUser, data) { - const template = emailTemplates[data.template] - const dataCC = data.cc || [] - const templateCC = template.cc || [] - const dataRecipients = data.recipients || [] - const templateRecipients = template.recipients || [] +async function sendEmail(currentUser, data) { + const template = emailTemplates[data.template]; + const dataCC = data.cc || []; + const templateCC = template.cc || []; + const dataRecipients = data.recipients || []; + const templateRecipients = template.recipients || []; const subjectBody = { subject: data.subject || template.subject, - body: data.body || template.body - } + body: data.body || template.body, + }; for (const key in subjectBody) { - subjectBody[key] = await helper.substituteStringByObject(subjectBody[key], data.data) + subjectBody[key] = await helper.substituteStringByObject( + subjectBody[key], + data.data + ); } const emailData = { // override template if coming data already have the 'from' address @@ -336,21 +407,27 @@ async function sendEmail (currentUser, data) { cc: _.uniq([...dataCC, ...templateCC]), data: { ...data.data, ...subjectBody }, sendgrid_template_id: template.sendgridTemplateId, - version: 'v3' - } - await helper.postEvent(config.EMAIL_TOPIC, emailData) + version: 'v3', + }; + await helper.postEvent(config.EMAIL_TOPIC, emailData); } -sendEmail.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - data: Joi.object().keys({ - template: Joi.string().valid(...Object.keys(emailTemplates)).required(), - data: Joi.object().required(), - from: Joi.string().email(), - recipients: Joi.array().items(Joi.string().email()).allow(null), - cc: Joi.array().items(Joi.string().email()).allow(null) - }).required() -}).required() +sendEmail.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + data: Joi.object() + .keys({ + template: Joi.string() + .valid(...Object.keys(emailTemplates)) + .required(), + data: Joi.object().required(), + from: Joi.string().email(), + recipients: Joi.array().items(Joi.string().email()).allow(null), + cc: Joi.array().items(Joi.string().email()).allow(null), + }) + .required(), + }) + .required(); /** * Add a member to a team as customer. @@ -360,25 +437,25 @@ sendEmail.schema = Joi.object().keys({ * @param {String} fields the fields to be returned * @returns {Object} the member added */ -async function _addMemberToProjectAsCustomer (projectId, userId, fields) { +async function _addMemberToProjectAsCustomer(projectId, userId, fields) { try { const member = await helper.createProjectMember( projectId, { userId: userId, role: 'customer' }, { fields } - ) - return member + ); + return member; } catch (err) { - err.message = _.get(err, 'response.body.message') || err.message + err.message = _.get(err, 'response.body.message') || err.message; if (err.message && err.message.includes('User already registered')) { - throw new Error('User is already added') + throw new Error('User is already added'); } logger.error({ component: 'TeamService', context: '_addMemberToProjectAsCustomer', - message: err.message - }) - throw err + message: err.message, + }); + throw err; } } @@ -390,81 +467,112 @@ async function _addMemberToProjectAsCustomer (projectId, userId, fields) { * @param {Object} data the object including members with handle/email to be added * @returns {Object} the success/failed added members */ -async function addMembers (currentUser, id, criteria, data) { - await helper.getProjectById(currentUser, id) // check whether the user can access the project +async function addMembers(currentUser, id, criteria, data) { + await helper.getProjectById(currentUser, id); // check whether the user can access the project const result = { success: [], - failed: [] - } - - const handles = data.handles || [] - const emails = data.emails || [] - - const handleMembers = await helper.getMemberDetailsByHandles(handles) - .then((members) => _.map(members, (member) => ({ - ...member, - // populate members with lower-cased handle for case insensitive search - handleLowerCase: member.handle.toLowerCase() - }))) - - const emailMembers = await helper.getMemberDetailsByEmails(emails) - .then((members) => _.map(members, (member) => ({ - ...member, - // populate members with lower-cased email for case insensitive search - emailLowerCase: member.email.toLowerCase() - }))) + failed: [], + }; + + const handles = data.handles || []; + const emails = data.emails || []; + + const handleMembers = await helper + .getMemberDetailsByHandles(handles) + .then((members) => + _.map(members, (member) => ({ + ...member, + // populate members with lower-cased handle for case insensitive search + handleLowerCase: member.handle.toLowerCase(), + })) + ); + + const emailMembers = await helper + .getMemberDetailsByEmails(emails) + .then((members) => + _.map(members, (member) => ({ + ...member, + // populate members with lower-cased email for case insensitive search + emailLowerCase: member.email.toLowerCase(), + })) + ); await Promise.all([ - Promise.all(handles.map(handle => { - const memberDetails = _.find(handleMembers, { handleLowerCase: handle.toLowerCase() }) - - if (!memberDetails) { - result.failed.push({ error: 'User doesn\'t exist', handle }) - return - } - - return _addMemberToProjectAsCustomer(id, memberDetails.userId, criteria.fields) - .then(member => { - // note, that we return `handle` in the same case it was in request - result.success.push(({ ...member, handle })) - }).catch(err => { - result.failed.push({ error: err.message, handle }) - }) - })), - - Promise.all(emails.map(email => { - const memberDetails = _.find(emailMembers, { emailLowerCase: email.toLowerCase() }) - - if (!memberDetails) { - result.failed.push({ error: 'User doesn\'t exist', email }) - return - } + Promise.all( + handles.map((handle) => { + const memberDetails = _.find(handleMembers, { + handleLowerCase: handle.toLowerCase(), + }); + + if (!memberDetails) { + result.failed.push({ error: "User doesn't exist", handle }); + return; + } - return _addMemberToProjectAsCustomer(id, memberDetails.id, criteria.fields) - .then(member => { - // note, that we return `email` in the same case it was in request - result.success.push(({ ...member, email })) - }).catch(err => { - result.failed.push({ error: err.message, email }) - }) - })) - ]) + return _addMemberToProjectAsCustomer( + id, + memberDetails.userId, + criteria.fields + ) + .then((member) => { + // note, that we return `handle` in the same case it was in request + result.success.push({ ...member, handle }); + }) + .catch((err) => { + result.failed.push({ error: err.message, handle }); + }); + }) + ), + + Promise.all( + emails.map((email) => { + const memberDetails = _.find(emailMembers, { + emailLowerCase: email.toLowerCase(), + }); + + if (!memberDetails) { + result.failed.push({ error: "User doesn't exist", email }); + return; + } - return result + return _addMemberToProjectAsCustomer( + id, + memberDetails.id, + criteria.fields + ) + .then((member) => { + // note, that we return `email` in the same case it was in request + result.success.push({ ...member, email }); + }) + .catch((err) => { + result.failed.push({ error: err.message, email }); + }); + }) + ), + ]); + + return result; } -addMembers.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required(), - criteria: Joi.object().keys({ - fields: Joi.string() - }).required(), - data: Joi.object().keys({ - handles: Joi.array().items(Joi.string()), - emails: Joi.array().items(Joi.string().email()) - }).or('handles', 'emails').required() -}).required() +addMembers.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + criteria: Joi.object() + .keys({ + fields: Joi.string(), + }) + .required(), + data: Joi.object() + .keys({ + handles: Joi.array().items(Joi.string()), + emails: Joi.array().items(Joi.string().email()), + }) + .or('handles', 'emails') + .required(), + }) + .required(); /** * Search members in a team. @@ -475,19 +583,23 @@ addMembers.schema = Joi.object().keys({ * @params {Object} criteria the search criteria * @returns {Object} the search result */ -async function searchMembers (currentUser, id, criteria) { - const result = await helper.listProjectMembers(currentUser, id, criteria) - return { result } +async function searchMembers(currentUser, id, criteria) { + const result = await helper.listProjectMembers(currentUser, id, criteria); + return { result }; } -searchMembers.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required(), - criteria: Joi.object().keys({ - role: Joi.string(), - fields: Joi.string() - }).required() -}).required() +searchMembers.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + criteria: Joi.object() + .keys({ + role: Joi.string(), + fields: Joi.string(), + }) + .required(), + }) + .required(); /** * Search member invites for a team. @@ -498,18 +610,26 @@ searchMembers.schema = Joi.object().keys({ * @params {Object} criteria the search criteria * @returns {Object} the search result */ -async function searchInvites (currentUser, id, criteria) { - const result = await helper.listProjectMemberInvites(currentUser, id, criteria) - return { result } +async function searchInvites(currentUser, id, criteria) { + const result = await helper.listProjectMemberInvites( + currentUser, + id, + criteria + ); + return { result }; } -searchInvites.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required(), - criteria: Joi.object().keys({ - fields: Joi.string() - }).required() -}).required() +searchInvites.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + criteria: Joi.object() + .keys({ + fields: Joi.string(), + }) + .required(), + }) + .required(); /** * Remove a member from a team. @@ -520,15 +640,17 @@ searchInvites.schema = Joi.object().keys({ * @param {String} projectMemberId the id of the project member * @returns {undefined} */ -async function deleteMember (currentUser, id, projectMemberId) { - await helper.deleteProjectMember(currentUser, id, projectMemberId) +async function deleteMember(currentUser, id, projectMemberId) { + await helper.deleteProjectMember(currentUser, id, projectMemberId); } -deleteMember.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required(), - projectMemberId: Joi.number().integer().required() -}).required() +deleteMember.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + projectMemberId: Joi.number().integer().required(), + }) + .required(); /** * Return details about the current user. @@ -537,13 +659,31 @@ deleteMember.schema = Joi.object().keys({ * @params {Object} criteria the search criteria * @returns {Object} the user data for current user */ -async function getMe (currentUser) { - return helper.getUserByExternalId(currentUser.userId) +async function getMe(currentUser) { + return helper.getUserByExternalId(currentUser.userId); } -getMe.schema = Joi.object().keys({ - currentUser: Joi.object().required() -}).required() +getMe.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + }) + .required(); + +/** + * @param {Object} currentUser the user performing the operation. + * @param {Object} data project data + * @returns {Object} the created project + */ +async function createProj(currentUser, data) { + return helper.createProject(currentUser, data); +} + +createProj.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + data: Joi.object().required(), + }) + .required(); module.exports = { searchTeams, @@ -554,5 +694,6 @@ module.exports = { searchMembers, searchInvites, deleteMember, - getMe -} + getMe, + createProj, +}; From b16a6fc68707155c2c923336689ddcb7621dd96a Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Thu, 27 May 2021 12:14:58 +0300 Subject: [PATCH 04/23] chore: use v5/members instead of v3/members --- config/default.js | 2 +- src/common/helper.js | 32 +++++++++++++------------------- src/services/PaymentService.js | 2 +- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/config/default.js b/config/default.js index 2b5ca7ba..1a3090bf 100644 --- a/config/default.js +++ b/config/default.js @@ -40,7 +40,7 @@ module.exports = { TOPCODER_USERS_API: process.env.TOPCODER_USERS_API || 'https://api.topcoder-dev.com/v3/users', // the api to find topcoder members - TOPCODER_MEMBERS_API: process.env.TOPCODER_MEMBERS_API || 'https://api.topcoder-dev.com/v3/members', + TOPCODER_MEMBERS_API: process.env.TOPCODER_MEMBERS_API || 'https://api.topcoder-dev.com/v5/members', // rate limit of requests to user api MAX_PARALLEL_REQUEST_TOPCODER_USERS_API: process.env.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API || 100, diff --git a/src/common/helper.js b/src/common/helper.js index 0ce11905..e68e5c58 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1304,13 +1304,10 @@ async function getMemberDetailsByHandles(handles) { } const token = await getM2MToken(); const res = await request - .get(`${config.TOPCODER_MEMBERS_API}/_search`) + .get(`${config.TOPCODER_MEMBERS_API}/`) .query({ - query: _.map( - handles, - (handle) => `handleLower:${handle.toLowerCase()}` - ).join(' OR '), - fields: 'userId,handle,firstName,lastName,email', + 'handlesLower[]': handles.map(handle => handle.toLowerCase()), + fields: 'userId,handle,handleLower,firstName,lastName,email', }) .set('Authorization', `Bearer ${token}`) .set('Accept', 'application/json'); @@ -1318,7 +1315,7 @@ async function getMemberDetailsByHandles(handles) { context: 'getMemberDetailsByHandles', message: `response body: ${JSON.stringify(res.body)}`, }); - return _.get(res.body, 'result.content'); + return res.body; } /** @@ -1327,17 +1324,14 @@ async function getMemberDetailsByHandles(handles) { * @param {String} handle the user handle * @returns {Object} the member details */ -async function getV3MemberDetailsByHandle(handle) { - const token = await getM2MToken(); - const res = await request - .get(`${config.TOPCODER_MEMBERS_API}/${handle}`) - .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); - localLogger.debug({ - context: 'getV3MemberDetailsByHandle', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res.body, 'result.content'); +async function getMemberDetailsByHandle(handle) { + const [memberDetails] = await getMemberDetailsByHandles([handle]) + + if (!memberDetails) { + throw new errors.NotFoundError(`Member details are not found by handle "${handle}".`) + } + + return memberDetails } /** @@ -1756,7 +1750,7 @@ module.exports = { getAuditM2Muser, checkIsMemberOfProject, getMemberDetailsByHandles, - getV3MemberDetailsByHandle, + getMemberDetailsByHandle, getMemberDetailsByEmails, createProjectMember, listProjectMembers, diff --git a/src/services/PaymentService.js b/src/services/PaymentService.js index d06ad671..e61a6c34 100644 --- a/src/services/PaymentService.js +++ b/src/services/PaymentService.js @@ -153,7 +153,7 @@ async function activateChallenge (id, token) { async function closeChallenge (id, userHandle, token) { localLogger.info({ context: 'closeChallenge', message: `Closing challenge ${id}` }) try { - const { userId } = await helper.getV3MemberDetailsByHandle(userHandle) + const { userId } = await helper.getMemberDetailsByHandle(userHandle) const body = { status: constants.ChallengeStatus.COMPLETED, winners: [{ From 3ede43e70d7f0111950e3e3f5c8faf0e98f1b18b Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Thu, 27 May 2021 15:49:58 +0530 Subject: [PATCH 05/23] fix: addded new 'offred' status --- src/bootstrap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bootstrap.js b/src/bootstrap.js index 2999f131..8bfa6df2 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -16,7 +16,7 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') -Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed') +Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed','offered') Joi.title = () => Joi.string().max(128) Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) From 93495aa0871333e8bd8ed4d698d8b634770f7e1b Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Thu, 27 May 2021 18:36:42 +0530 Subject: [PATCH 06/23] Updated Swagger for jobCandidate statuses --- docs/swagger.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a0b6064b..3bd72c09 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -603,6 +603,13 @@ paths: "cancelled", "interview", "topcoder-rejected", + "applied", + "rejected-pre-screen", + "skills-test", + "skills-test", + "phone-screen", + "job-closed", + "offered" ] description: The job candidate status. - in: query From 8446c87052640e2db0f7c57bf80aab131a393639 Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Thu, 27 May 2021 19:07:47 +0530 Subject: [PATCH 07/23] Update swagger.yaml --- docs/swagger.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 3bd72c09..d209a3fb 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -606,7 +606,6 @@ paths: "applied", "rejected-pre-screen", "skills-test", - "skills-test", "phone-screen", "job-closed", "offered" From 01faad42292d91cd36db27b2e07f306eea6084a3 Mon Sep 17 00:00:00 2001 From: dengjun Date: Sat, 29 May 2021 12:11:35 +0800 Subject: [PATCH 08/23] api-updates: challnege:30186701 --- data/demo-data.json | 69 +- ...coder-bookings-api.postman_collection.json | 157 ++- docs/swagger.yaml | 106 +- ...-28-add-fields-to-job-and-job-candidate.js | 55 + src/bootstrap.js | 2 +- src/common/helper.js | 1083 +++++++++-------- src/controllers/TeamController.js | 62 +- src/models/Job.js | 30 + src/models/JobCandidate.js | 3 + src/routes/TeamRoutes.js | 48 +- src/services/InterviewService.js | 4 +- src/services/JobCandidateService.js | 9 +- src/services/JobService.js | 24 +- src/services/ResourceBookingService.js | 1 + src/services/TeamService.js | 390 +++--- 15 files changed, 1159 insertions(+), 884 deletions(-) create mode 100644 migrations/2021-05-28-add-fields-to-job-and-job-candidate.js diff --git a/data/demo-data.json b/data/demo-data.json index e0733443..30f547ae 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -20,6 +20,12 @@ ], "status": "in-review", "isApplicationPageActive": false, + "minSalary": 100, + "maxSalary": 200, + "hoursPerWeek": 20, + "jobLocation": "Any location", + "jobTimezone": "GMT", + "currency": "USD", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:21:10.394Z", @@ -45,6 +51,12 @@ ], "status": "in-review", "isApplicationPageActive": false, + "minSalary": 100, + "maxSalary": 200, + "hoursPerWeek": 20, + "jobLocation": "Any location", + "jobTimezone": "GMT", + "currency": "USD", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:11:26.934Z", @@ -70,6 +82,12 @@ ], "status": "in-review", "isApplicationPageActive": false, + "minSalary": 100, + "maxSalary": 200, + "hoursPerWeek": 20, + "jobLocation": "Any location", + "jobTimezone": "GMT", + "currency": "USD", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:18.595Z", @@ -95,6 +113,12 @@ ], "status": "in-review", "isApplicationPageActive": false, + "minSalary": 100, + "maxSalary": 200, + "hoursPerWeek": 20, + "jobLocation": "Any location", + "jobTimezone": "GMT", + "currency": "USD", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:12:09.293Z", @@ -109,6 +133,7 @@ "status": "open", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:14:05.412Z", @@ -122,6 +147,7 @@ "status": "open", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:14:41.500Z", @@ -135,6 +161,7 @@ "status": "open", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:14:43.985Z", @@ -148,6 +175,7 @@ "status": "open", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:14:46.310Z", @@ -161,6 +189,7 @@ "status": "open", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:14:48.449Z", @@ -174,6 +203,7 @@ "status": "open", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:14:50.595Z", @@ -184,7 +214,7 @@ "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", "googleCalendarId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, "attendeesList": null, @@ -203,6 +233,7 @@ "status": "open", "externalId": "88774631", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:16:34.914Z", @@ -213,7 +244,7 @@ "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", "googleCalendarId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, "attendeesList": null, @@ -228,7 +259,7 @@ "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", "googleCalendarId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, "attendeesList": null, @@ -247,6 +278,7 @@ "status": "open", "externalId": "88774631", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:21:13.939Z", @@ -257,7 +289,7 @@ "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", "googleCalendarId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 3, "startTimestamp": null, "attendeesList": [ @@ -275,7 +307,7 @@ "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", "googleCalendarId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, "attendeesList": [ @@ -293,7 +325,7 @@ "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", "googleCalendarId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, "attendeesList": null, @@ -312,6 +344,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:23.420Z", @@ -325,6 +358,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:41.691Z", @@ -338,6 +372,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:35.819Z", @@ -351,6 +386,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:39.914Z", @@ -364,6 +400,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:37.962Z", @@ -377,6 +414,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:43.611Z", @@ -390,6 +428,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:47.468Z", @@ -403,6 +442,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:45.506Z", @@ -416,6 +456,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:57.355Z", @@ -429,6 +470,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:49.415Z", @@ -442,6 +484,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:51.500Z", @@ -455,6 +498,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:53.297Z", @@ -468,6 +512,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:55.300Z", @@ -481,6 +526,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:59.282Z", @@ -494,6 +540,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:24:01.429Z", @@ -507,6 +554,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:24:03.511Z", @@ -520,6 +568,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:24:05.605Z", @@ -533,6 +582,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:15:05.339Z", @@ -546,6 +596,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:15:03.606Z", @@ -559,6 +610,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:14:59.106Z", @@ -572,6 +624,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:15:02.183Z", @@ -585,6 +638,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:15:00.676Z", @@ -598,6 +652,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:15:06.739Z", @@ -2053,4 +2108,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 74155753..4070d520 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", + "_postman_id": "ca01f845-9ba8-473d-b005-fc200ac3cd39", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -33,7 +33,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -77,7 +77,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -103,8 +103,7 @@ "listen": "test", "script": { "exec": [ - "var data = JSON.parse(responseBody);\r", - "postman.setEnvironmentVariable(\"jobId\",data.id);" + "var data = JSON.parse(responseBody);" ], "type": "text/javascript" } @@ -121,7 +120,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -165,7 +164,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -208,7 +207,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -251,7 +250,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -991,7 +990,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1024,7 +1023,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1057,7 +1056,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1103,7 +1102,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description updated\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description updated\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1136,7 +1135,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description updated\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description updated\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1169,7 +1168,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1202,7 +1201,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1235,7 +1234,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1268,7 +1267,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1301,7 +1300,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1347,7 +1346,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description updated 2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description updated 2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1380,7 +1379,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description updated 2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description updated 2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1413,7 +1412,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1446,7 +1445,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1707,7 +1706,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -1751,7 +1750,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobIdCreatedByM2M}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobIdCreatedByM2M}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -1795,7 +1794,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -1839,7 +1838,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -1883,7 +1882,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -1926,7 +1925,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2672,7 +2671,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2705,7 +2704,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobIdCreatedByM2M}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobIdCreatedByM2M}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2738,7 +2737,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2771,7 +2770,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2804,7 +2803,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2837,7 +2836,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2870,7 +2869,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2903,7 +2902,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2936,7 +2935,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2969,7 +2968,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -3002,7 +3001,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -3035,7 +3034,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -3225,7 +3224,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -3274,7 +3273,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -3323,7 +3322,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -3372,7 +3371,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -3421,7 +3420,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -3470,7 +3469,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -7946,7 +7945,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -7995,7 +7994,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -11436,7 +11435,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -14739,7 +14738,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -18052,7 +18051,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job1\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"ee4c50c1-c8c3-475e-b6b6-edbd136a19d6\",\n \"89139c80-d0a2-47c2-aa16-14589d5afd10\",\n \"9f2d9127-6a2e-4506-ad76-c4ab63577b09\",\n \"9515e7ee-83b6-49d1-ba5c-6c59c5a8ef1b\",\n \"c854ab55-5922-4be1-8ecc-b3bc1f8629af\",\n \"8456002e-fa2d-44f0-b0e7-86b1c02b6e4c\",\n \"114b4ec8-805e-4c60-b351-14a955a991a9\",\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\",\n \"23839f38-6f19-4de9-9d28-f020056bca73\",\n \"289e42a3-23e9-49be-88e1-6deb93cd8c31\",\n \"b403f209-63b5-42bc-9b5f-1564416640d8\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job1\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"ee4c50c1-c8c3-475e-b6b6-edbd136a19d6\",\n \"89139c80-d0a2-47c2-aa16-14589d5afd10\",\n \"9f2d9127-6a2e-4506-ad76-c4ab63577b09\",\n \"9515e7ee-83b6-49d1-ba5c-6c59c5a8ef1b\",\n \"c854ab55-5922-4be1-8ecc-b3bc1f8629af\",\n \"8456002e-fa2d-44f0-b0e7-86b1c02b6e4c\",\n \"114b4ec8-805e-4c60-b351-14a955a991a9\",\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\",\n \"23839f38-6f19-4de9-9d28-f020056bca73\",\n \"289e42a3-23e9-49be-88e1-6deb93cd8c31\",\n \"b403f209-63b5-42bc-9b5f-1564416640d8\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -18106,7 +18105,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\",\n \"23839f38-6f19-4de9-9d28-f020056bca73\",\n \"289e42a3-23e9-49be-88e1-6deb93cd8c31\",\n \"b403f209-63b5-42bc-9b5f-1564416640d8\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\",\n \"23839f38-6f19-4de9-9d28-f020056bca73\",\n \"289e42a3-23e9-49be-88e1-6deb93cd8c31\",\n \"b403f209-63b5-42bc-9b5f-1564416640d8\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -18756,7 +18755,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job3\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job3\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -20393,7 +20392,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job4\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"8456002e-fa2d-44f0-b0e7-86b1c02b6e4c\",\n \"114b4ec8-805e-4c60-b351-14a955a991a9\",\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job4\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"8456002e-fa2d-44f0-b0e7-86b1c02b6e4c\",\n \"114b4ec8-805e-4c60-b351-14a955a991a9\",\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -20493,7 +20492,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job5\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job5\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -20596,7 +20595,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -20747,7 +20746,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -20780,7 +20779,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -20979,7 +20978,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"jobId\": \"{{job_id_created_by_administrator}}\",\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"status\": \"placed\"\r\n}", + "raw": "{\r\n \"jobId\": \"{{job_id_created_by_administrator}}\",\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"status\": \"placed\",\r\n \"remark\": \"excellent\"\r\n}", "options": { "raw": { "language": "json" @@ -21098,7 +21097,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -21461,7 +21460,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -21757,7 +21756,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -22156,7 +22155,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_17234}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_17234}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -22577,7 +22576,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -22727,7 +22726,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -22760,7 +22759,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -23007,7 +23006,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"jobId\": \"{{job_id_created_by_member}}\",\r\n \"userId\": \"fe38eed1-af73-41fd-85a2-ac4da1ff09a3\",\r\n \"status\": \"placed\"\r\n}", + "raw": "{\r\n \"jobId\": \"{{job_id_created_by_member}}\",\r\n \"userId\": \"fe38eed1-af73-41fd-85a2-ac4da1ff09a3\",\r\n \"status\": \"placed\",\r\n \"remark\": \"excellent\"\r\n}", "options": { "raw": { "language": "json" @@ -23126,7 +23125,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -23541,7 +23540,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -23885,7 +23884,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -24342,7 +24341,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_17234}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_17234}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -24818,7 +24817,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -25250,7 +25249,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"jobId\": \"{{job_id_created_by_connect_manager}}\",\r\n \"userId\": \"fe38eed1-af73-41fd-85a2-ac4da1ff09a3\",\r\n \"status\": \"placed\"\r\n}", + "raw": "{\r\n \"jobId\": \"{{job_id_created_by_connect_manager}}\",\r\n \"userId\": \"fe38eed1-af73-41fd-85a2-ac4da1ff09a3\",\r\n \"status\": \"placed\",\r\n \"remark\": \"excellent\"\r\n}", "options": { "raw": { "language": "json" @@ -25369,7 +25368,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -25784,7 +25783,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -26124,7 +26123,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -26575,7 +26574,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_17234}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_17234}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -27007,4 +27006,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1584f5cf..4cbce8f0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -613,7 +613,7 @@ paths: "skills-test", "phone-screen", "job-closed", - "offered" + "offered", ] description: The job candidate status. - in: query @@ -3290,6 +3290,12 @@ components: - numPositions - skills - status + - minSalary + - maxSalary + - hoursPerWeek + - jobLocation + - jobTimezone + - currency - createdAt - createdBy properties: @@ -3358,6 +3364,30 @@ components: isApplicationPageActive: type: boolean default: false + minSalary: + type: integer + example: 1000 + description: "the amount of minimum salary" + maxSalary: + type: integer + example: 3000 + description: "the amount of maximum salary" + hoursPerWeek: + type: integer + example: 20 + description: "the amount working hours per week" + jobLocation: + type: string + example: "Any location" + description: "the location of job" + jobTimezone: + type: string + example: "GMT" + description: "the timezone of job" + currency: + type: string + example: "USD" + description: "the currency of job" createdAt: type: string format: date-time @@ -3389,6 +3419,12 @@ components: - title - numPositions - skills + - minSalary + - maxSalary + - hoursPerWeek + - jobLocation + - jobTimezone + - currency properties: projectId: type: integer @@ -3447,6 +3483,30 @@ components: isApplicationPageActive: type: boolean default: false + minSalary: + type: integer + example: 1000 + description: "the amount of minimum salary" + maxSalary: + type: integer + example: 3000 + description: "the amount of maximum salary" + hoursPerWeek: + type: integer + example: 20 + description: "the amount working hours per week" + jobLocation: + type: string + example: "Any location" + description: "the location of job" + jobTimezone: + type: string + example: "GMT" + description: "the timezone of job" + currency: + type: string + example: "USD" + description: "the currency of job" JobCandidate: required: - id @@ -3490,6 +3550,10 @@ components: type: string example: "http://example.com" description: "The resume link" + remark: + type: string + example: "excellent" + description: "The remark of candidate" createdAt: type: string format: date-time @@ -3546,6 +3610,10 @@ components: type: string example: "http://example.com" description: "The resume link" + remark: + type: string + example: "excellent" + description: "The remark of candidate" interviews: type: array description: "Interviews associated to this job candidate." @@ -3605,6 +3673,10 @@ components: type: string example: "http://example.com" description: "The resume link" + remark: + type: string + example: "excellent" + description: "The remark of candidate" JobCandidatePatchRequestBody: properties: status: @@ -3629,6 +3701,10 @@ components: type: string example: "http://example.com" description: "The resume link" + remark: + type: string + example: "excellent" + description: "The remark of candidate" Interview: required: - id @@ -3888,6 +3964,30 @@ components: isApplicationPageActive: type: boolean default: false + minSalary: + type: integer + example: 1000 + description: "the amount of minimum salary" + maxSalary: + type: integer + example: 3000 + description: "the amount of maximum salary" + hoursPerWeek: + type: integer + example: 20 + description: "the amount working hours per week" + jobLocation: + type: string + example: "Any location" + description: "the location of job" + jobTimezone: + type: string + example: "GMT" + description: "the timezone of job" + currency: + type: string + example: "USD" + description: "the currency of job" ResourceBooking: required: - id @@ -4643,6 +4743,10 @@ components: type: string format: url description: "The link for the resume that can be downloaded" + remark: + type: string + example: "excellent" + description: "The remark of candidate" interviews: type: array items: diff --git a/migrations/2021-05-28-add-fields-to-job-and-job-candidate.js b/migrations/2021-05-28-add-fields-to-job-and-job-candidate.js new file mode 100644 index 00000000..472a25a8 --- /dev/null +++ b/migrations/2021-05-28-add-fields-to-job-and-job-candidate.js @@ -0,0 +1,55 @@ +const config = require('config') + +/* + * Add min_salary, max_salary, hours_per_week, job_location, job_timezone and currency fields to the Job model. + * Add remark field to the JobCandidate model. + */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'min_salary', + { type: Sequelize.INTEGER, allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'max_salary', + { type: Sequelize.INTEGER, allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'hours_per_week', + { type: Sequelize.INTEGER, allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'job_location', + { type: Sequelize.STRING(255), allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'job_timezone', + { type: Sequelize.STRING(128), allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'currency', + { type: Sequelize.STRING(30), allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'job_candidates', schema: config.DB_SCHEMA_NAME }, 'remark', + { type: Sequelize.STRING(255) }, + { transaction }) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + }, + down: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'min_salary', { transaction }) + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'max_salary', { transaction }) + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'hours_per_week', { transaction }) + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'job_location', { transaction }) + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'job_timezone', { transaction }) + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'currency', { transaction }) + await queryInterface.removeColumn({ tableName: 'job_candidates', schema: config.DB_SCHEMA_NAME }, 'remark', { transaction }) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + } +} diff --git a/src/bootstrap.js b/src/bootstrap.js index 8bfa6df2..2a1571c2 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -16,7 +16,7 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') -Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed','offered') +Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed', 'offered') Joi.title = () => Joi.string().max(128) Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) diff --git a/src/common/helper.js b/src/common/helper.js index e68e5c58..f3b950b0 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -2,50 +2,50 @@ * This file defines helper methods */ -const fs = require('fs'); -const querystring = require('querystring'); -const Confirm = require('prompt-confirm'); -const Bottleneck = require('bottleneck'); -const AWS = require('aws-sdk'); -const config = require('config'); -const HttpStatus = require('http-status-codes'); -const _ = require('lodash'); -const request = require('superagent'); -const elasticsearch = require('@elastic/elasticsearch'); +const fs = require('fs') +const querystring = require('querystring') +const Confirm = require('prompt-confirm') +const Bottleneck = require('bottleneck') +const AWS = require('aws-sdk') +const config = require('config') +const HttpStatus = require('http-status-codes') +const _ = require('lodash') +const request = require('superagent') +const elasticsearch = require('@elastic/elasticsearch') const { - ResponseError: ESResponseError, -} = require('@elastic/elasticsearch/lib/errors'); -const errors = require('../common/errors'); -const logger = require('./logger'); -const models = require('../models'); -const eventDispatcher = require('./eventDispatcher'); -const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper'); -const moment = require('moment'); + ResponseError: ESResponseError +} = require('@elastic/elasticsearch/lib/errors') +const errors = require('../common/errors') +const logger = require('./logger') +const models = require('../models') +const eventDispatcher = require('./eventDispatcher') +const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') +const moment = require('moment') const localLogger = { debug: (message) => logger.debug({ component: 'helper', context: message.context, - message: message.message, + message: message.message }), error: (message) => logger.error({ component: 'helper', context: message.context, - message: message.message, + message: message.message }), info: (message) => logger.info({ component: 'helper', context: message.context, - message: message.message, - }), -}; + message: message.message + }) +} -AWS.config.region = config.esConfig.AWS_REGION; +AWS.config.region = config.esConfig.AWS_REGION -const m2mAuth = require('tc-core-library-js').auth.m2m; +const m2mAuth = require('tc-core-library-js').auth.m2m const m2m = m2mAuth( _.pick(config, [ @@ -53,9 +53,9 @@ const m2m = m2mAuth( 'AUTH0_AUDIENCE', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', - 'AUTH0_PROXY_SERVER_URL', + 'AUTH0_PROXY_SERVER_URL' ]) -); +) const m2mForUbahn = m2mAuth({ AUTH0_AUDIENCE: config.AUTH0_AUDIENCE_UBAHN, @@ -64,20 +64,20 @@ const m2mForUbahn = m2mAuth({ 'TOKEN_CACHE_TIME', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', - 'AUTH0_PROXY_SERVER_URL', - ]), -}); + 'AUTH0_PROXY_SERVER_URL' + ]) +}) -let busApiClient; +let busApiClient /** * Get bus api client. * * @returns {Object} the bus api client */ -function getBusApiClient() { +function getBusApiClient () { if (busApiClient) { - return busApiClient; + return busApiClient } busApiClient = busApi( _.pick(config, [ @@ -88,17 +88,17 @@ function getBusApiClient() { 'AUTH0_CLIENT_SECRET', 'BUSAPI_URL', 'KAFKA_ERROR_TOPIC', - 'AUTH0_PROXY_SERVER_URL', + 'AUTH0_PROXY_SERVER_URL' ]) - ); - return busApiClient; + ) + return busApiClient } // ES Client mapping -const esClients = {}; +const esClients = {} // The es index property mapping -const esIndexPropertyMapping = {}; +const esIndexPropertyMapping = {} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { projectId: { type: 'integer' }, externalId: { type: 'keyword' }, @@ -113,17 +113,24 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { skills: { type: 'keyword' }, status: { type: 'keyword' }, isApplicationPageActive: { type: 'boolean' }, + minSalary: { type: 'integer' }, + maxSalary: { type: 'integer' }, + hoursPerWeek: { type: 'integer' }, + jobLocation: { type: 'keyword' }, + jobTimezone: { type: 'keyword' }, + currency: { type: 'keyword' }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { jobId: { type: 'keyword' }, userId: { type: 'keyword' }, status: { type: 'keyword' }, externalId: { type: 'keyword' }, resume: { type: 'text' }, + remark: { type: 'keyword' }, interviews: { type: 'nested', properties: { @@ -150,14 +157,14 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, updatedBy: { type: 'keyword' }, - deletedAt: { type: 'date' }, - }, + deletedAt: { type: 'date' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { projectId: { type: 'integer' }, userId: { type: 'keyword' }, @@ -195,32 +202,32 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, - }, + updatedBy: { type: 'keyword' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, - }, + updatedBy: { type: 'keyword' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} /** * Get the first parameter from cli arguments */ -function getParamFromCliArgs() { - const filteredArgs = process.argv.filter((arg) => !arg.includes('--')); +function getParamFromCliArgs () { + const filteredArgs = process.argv.filter((arg) => !arg.includes('--')) if (filteredArgs.length > 2) { - return filteredArgs[2]; + return filteredArgs[2] } - return null; + return null } /** @@ -228,18 +235,18 @@ function getParamFromCliArgs() { * @param {string} promptQuery the query to ask the user * @param {function} cb the callback function */ -async function promptUser(promptQuery, cb) { +async function promptUser (promptQuery, cb) { if (process.argv.includes('--force')) { - await cb(); - return; + await cb() + return } - const prompt = new Confirm(promptQuery); + const prompt = new Confirm(promptQuery) prompt.ask(async (answer) => { if (answer) { - await cb(); + await cb() } - }); + }) } /** @@ -248,23 +255,23 @@ async function promptUser(promptQuery, cb) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function createIndex(index, logger, esClient = null) { +async function createIndex (index, logger, esClient = null) { if (!esClient) { - esClient = getESClient(); + esClient = getESClient() } await esClient.indices.create({ index, body: { mappings: { - properties: esIndexPropertyMapping[index], - }, - }, - }); + properties: esIndexPropertyMapping[index] + } + } + }) logger.info({ component: 'createIndex', - message: `ES Index ${index} creation succeeded!`, - }); + message: `ES Index ${index} creation succeeded!` + }) } /** @@ -273,45 +280,45 @@ async function createIndex(index, logger, esClient = null) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function deleteIndex(index, logger, esClient = null) { +async function deleteIndex (index, logger, esClient = null) { if (!esClient) { - esClient = getESClient(); + esClient = getESClient() } - await esClient.indices.delete({ index }); + await esClient.indices.delete({ index }) logger.info({ component: 'deleteIndex', - message: `ES Index ${index} deletion succeeded!`, - }); + message: `ES Index ${index} deletion succeeded!` + }) } /** * Split data into bulks * @param {Array} data the array of data to split */ -function getBulksFromDocuments(data) { - const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6; - const bulks = []; - let documentIndex = 0; - let currentBulkSize = 0; - let currentBulk = []; +function getBulksFromDocuments (data) { + const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6 + const bulks = [] + let documentIndex = 0 + let currentBulkSize = 0 + let currentBulk = [] while (true) { // break loop when parsed all documents if (documentIndex >= data.length) { - bulks.push(currentBulk); - break; + bulks.push(currentBulk) + break } // check if current document size is greater than the max bulk size, if so, throw error const currentDocumentSize = Buffer.byteLength( JSON.stringify(data[documentIndex]), 'utf-8' - ); + ) if (maxBytes < currentDocumentSize) { throw new Error( `Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.` - ); + ) } if ( @@ -320,17 +327,17 @@ function getBulksFromDocuments(data) { ) { // if adding the current document goes over the max bulk size OR goes over max number of docs // then push the current bulk to bulks array and reset the current bulk - bulks.push(currentBulk); - currentBulk = []; - currentBulkSize = 0; + bulks.push(currentBulk) + currentBulk = [] + currentBulkSize = 0 } else { // otherwise, add document to current bulk - currentBulk.push(data[documentIndex]); - currentBulkSize += currentDocumentSize; - documentIndex++; + currentBulk.push(data[documentIndex]) + currentBulkSize += currentDocumentSize + documentIndex++ } } - return bulks; + return bulks } /** @@ -339,57 +346,57 @@ function getBulksFromDocuments(data) { * @param {Object} indexName the index name * @param {Object} logger the logger object */ -async function indexBulkDataToES(modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); +async function indexBulkDataToES (modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) logger.info({ component: 'indexBulkDataToES', - message: `Reindexing of ${modelName}s started!`, - }); + message: `Reindexing of ${modelName}s started!` + }) - const esClient = getESClient(); + const esClient = getESClient() // clear index - const indexExistsRes = await esClient.indices.exists({ index: indexName }); + const indexExistsRes = await esClient.indices.exists({ index: indexName }) if (indexExistsRes.statusCode !== 404) { - await deleteIndex(indexName, logger, esClient); + await deleteIndex(indexName, logger, esClient) } - await createIndex(indexName, logger, esClient); + await createIndex(indexName, logger, esClient) // get data from db logger.info({ component: 'indexBulkDataToES', - message: 'Getting data from database', - }); - const model = models[modelName]; - const data = await model.findAll({ include }); - const rawObjects = _.map(data, (r) => r.toJSON()); + message: 'Getting data from database' + }) + const model = models[modelName] + const data = await model.findAll({ include }) + const rawObjects = _.map(data, (r) => r.toJSON()) if (_.isEmpty(rawObjects)) { logger.info({ component: 'indexBulkDataToES', - message: `No data in database for ${modelName}`, - }); - return; + message: `No data in database for ${modelName}` + }) + return } - const bulks = getBulksFromDocuments(rawObjects); + const bulks = getBulksFromDocuments(rawObjects) - const startTime = Date.now(); - let doneCount = 0; + const startTime = Date.now() + let doneCount = 0 for (const bulk of bulks) { // send bulk to esclient const body = bulk.flatMap((doc) => [ { index: { _index: indexName, _id: doc.id } }, - doc, - ]); - await esClient.bulk({ refresh: true, body }); - doneCount += bulk.length; + doc + ]) + await esClient.bulk({ refresh: true, body }) + doneCount += bulk.length // log metrics - const timeSpent = Date.now() - startTime; - const avgTimePerDocument = timeSpent / doneCount; - const estimatedLength = avgTimePerDocument * data.length; - const timeLeft = startTime + estimatedLength - Date.now(); + const timeSpent = Date.now() - startTime + const avgTimePerDocument = timeSpent / doneCount + const estimatedLength = avgTimePerDocument * data.length + const timeLeft = startTime + estimatedLength - Date.now() logger.info({ component: 'indexBulkDataToES', message: `Processed ${doneCount} of ${ @@ -398,8 +405,8 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { avgTimePerDocument )}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime( timeLeft - )}`, - }); + )}` + }) } } @@ -410,36 +417,36 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { * @param {string} id the job id * @param {Object} logger the logger object */ -async function indexDataToEsById(id, modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); +async function indexDataToEsById (id, modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) logger.info({ component: 'indexDataToEsById', - message: `Reindexing of ${modelName} with id ${id} started!`, - }); - const esClient = getESClient(); + message: `Reindexing of ${modelName} with id ${id} started!` + }) + const esClient = getESClient() logger.info({ component: 'indexDataToEsById', - message: 'Getting data from database', - }); - const model = models[modelName]; + message: 'Getting data from database' + }) + const model = models[modelName] - const data = await model.findById(id, include); + const data = await model.findById(id, include) logger.info({ component: 'indexDataToEsById', - message: 'Indexing data into Elasticsearch', - }); + message: 'Indexing data into Elasticsearch' + }) await esClient.index({ index: indexName, id: id, - body: data.dataValues, - }); + body: data.dataValues + }) logger.info({ component: 'indexDataToEsById', - message: 'Indexing complete!', - }); + message: 'Indexing complete!' + }) } /** @@ -448,68 +455,68 @@ async function indexDataToEsById(id, modelOpts, indexName, logger) { * @param {Array} dataModels the data models to import * @param {Object} logger the logger object */ -async function importData(pathToFile, dataModels, logger) { +async function importData (pathToFile, dataModels, logger) { // check if file exists if (!fs.existsSync(pathToFile)) { - throw new Error(`File with path ${pathToFile} does not exist`); + throw new Error(`File with path ${pathToFile} does not exist`) } // clear database - logger.info({ component: 'importData', message: 'Clearing database...' }); - await models.sequelize.sync({ force: true }); + logger.info({ component: 'importData', message: 'Clearing database...' }) + await models.sequelize.sync({ force: true }) - let transaction = null; - let currentModelName = null; + let transaction = null + let currentModelName = null try { // Start a transaction - transaction = await models.sequelize.transaction(); - const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()); + transaction = await models.sequelize.transaction() + const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()) for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index]; - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); + const modelOpts = dataModels[index] + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) - currentModelName = modelName; - const model = models[modelName]; - const modelRecords = jsonData[modelName]; + currentModelName = modelName + const model = models[modelName] + const modelRecords = jsonData[modelName] if (modelRecords && modelRecords.length > 0) { logger.info({ component: 'importData', - message: `Importing data for model: ${modelName}`, - }); + message: `Importing data for model: ${modelName}` + }) - await model.bulkCreate(modelRecords, { include, transaction }); + await model.bulkCreate(modelRecords, { include, transaction }) logger.info({ component: 'importData', - message: `Records imported for model: ${modelName} = ${modelRecords.length}`, - }); + message: `Records imported for model: ${modelName} = ${modelRecords.length}` + }) } else { logger.info({ component: 'importData', - message: `No records to import for model: ${modelName}`, - }); + message: `No records to import for model: ${modelName}` + }) } } // commit transaction only if all things went ok logger.info({ component: 'importData', - message: 'committing transaction to database...', - }); - await transaction.commit(); + message: 'committing transaction to database...' + }) + await transaction.commit() } catch (error) { logger.error({ component: 'importData', - message: `Error while writing data of model: ${currentModelName}`, - }); + message: `Error while writing data of model: ${currentModelName}` + }) // rollback all insert operations if (transaction) { logger.info({ component: 'importData', - message: 'rollback database transaction...', - }); - transaction.rollback(); + message: 'rollback database transaction...' + }) + transaction.rollback() } if (error.name && error.errors && error.fields) { // For sequelize validation errors, we throw only fields with data that helps in debugging error, @@ -519,11 +526,11 @@ async function importData(pathToFile, dataModels, logger) { modelName: currentModelName, name: error.name, errors: error.errors, - fields: error.fields, + fields: error.fields }) - ); + ) } else { - throw error; + throw error } } @@ -533,10 +540,10 @@ async function importData(pathToFile, dataModels, logger) { include: [ { model: models.Interview, - as: 'interviews', - }, - ], - }; + as: 'interviews' + } + ] + } const resourceBookingModelOpts = { modelName: 'ResourceBooking', include: [ @@ -546,23 +553,23 @@ async function importData(pathToFile, dataModels, logger) { include: [ { model: models.WorkPeriodPayment, - as: 'payments', - }, - ], - }, - ], - }; - await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger); + as: 'payments' + } + ] + } + ] + } + await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) await indexBulkDataToES( jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger - ); + ) await indexBulkDataToES( resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger - ); + ) } /** @@ -571,74 +578,74 @@ async function importData(pathToFile, dataModels, logger) { * @param {Array} dataModels the data models to export * @param {Object} logger the logger object */ -async function exportData(pathToFile, dataModels, logger) { +async function exportData (pathToFile, dataModels, logger) { logger.info({ component: 'exportData', - message: `Start Saving data to file with path ${pathToFile}....`, - }); + message: `Start Saving data to file with path ${pathToFile}....` + }) - const allModelsRecords = {}; + const allModelsRecords = {} for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index]; - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); - const modelRecords = await models[modelName].findAll({ include }); - const rawRecords = _.map(modelRecords, (r) => r.toJSON()); - allModelsRecords[modelName] = rawRecords; + const modelOpts = dataModels[index] + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) + const modelRecords = await models[modelName].findAll({ include }) + const rawRecords = _.map(modelRecords, (r) => r.toJSON()) + allModelsRecords[modelName] = rawRecords logger.info({ component: 'exportData', - message: `Records loaded for model: ${modelName} = ${rawRecords.length}`, - }); + message: `Records loaded for model: ${modelName} = ${rawRecords.length}` + }) } - fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)); + fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)) logger.info({ component: 'exportData', - message: 'End Saving data to file....', - }); + message: 'End Saving data to file....' + }) } /** * Format a time in milliseconds into a human readable format * @param {Date} milliseconds the number of milliseconds */ -function formatTime(millisec) { - const ms = Math.floor(millisec % 1000); - const secs = Math.floor((millisec / 1000) % 60); - const mins = Math.floor((millisec / (1000 * 60)) % 60); - const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24); - const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7); - const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4); - const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12); - const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)); - - let formattedTime = '0 milliseconds'; +function formatTime (millisec) { + const ms = Math.floor(millisec % 1000) + const secs = Math.floor((millisec / 1000) % 60) + const mins = Math.floor((millisec / (1000 * 60)) % 60) + const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24) + const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7) + const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4) + const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12) + const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)) + + let formattedTime = '0 milliseconds' if (ms > 0) { - formattedTime = `${ms} milliseconds`; + formattedTime = `${ms} milliseconds` } if (secs > 0) { - formattedTime = `${secs} seconds ${formattedTime}`; + formattedTime = `${secs} seconds ${formattedTime}` } if (mins > 0) { - formattedTime = `${mins} minutes ${formattedTime}`; + formattedTime = `${mins} minutes ${formattedTime}` } if (hrs > 0) { - formattedTime = `${hrs} hours ${formattedTime}`; + formattedTime = `${hrs} hours ${formattedTime}` } if (days > 0) { - formattedTime = `${days} days ${formattedTime}`; + formattedTime = `${days} days ${formattedTime}` } if (weeks > 0) { - formattedTime = `${weeks} weeks ${formattedTime}`; + formattedTime = `${weeks} weeks ${formattedTime}` } if (mnths > 0) { - formattedTime = `${mnths} months ${formattedTime}`; + formattedTime = `${mnths} months ${formattedTime}` } if (yrs > 0) { - formattedTime = `${yrs} years ${formattedTime}`; + formattedTime = `${yrs} years ${formattedTime}` } - return formattedTime.trim(); + return formattedTime.trim() } /** @@ -647,30 +654,30 @@ function formatTime(millisec) { * @param {Array} source the array in which to search for the term * @param {Array | String} term the term to search */ -function checkIfExists(source, term) { - let terms; +function checkIfExists (source, term) { + let terms if (!_.isArray(source)) { - throw new Error('Source argument should be an array'); + throw new Error('Source argument should be an array') } - source = source.map((s) => s.toLowerCase()); + source = source.map((s) => s.toLowerCase()) if (_.isString(term)) { - terms = term.toLowerCase().split(' '); + terms = term.toLowerCase().split(' ') } else if (_.isArray(term)) { - terms = term.map((t) => t.toLowerCase()); + terms = term.map((t) => t.toLowerCase()) } else { - throw new Error('Term argument should be either a string or an array'); + throw new Error('Term argument should be either a string or an array') } for (let i = 0; i < terms.length; i++) { if (source.includes(terms[i])) { - return true; + return true } } - return false; + return false } /** @@ -678,10 +685,10 @@ function checkIfExists(source, term) { * @param {Function} fn the async function * @returns {Function} the wrapped function */ -function wrapExpress(fn) { +function wrapExpress (fn) { return function (req, res, next) { - fn(req, res, next).catch(next); - }; + fn(req, res, next).catch(next) + } } /** @@ -689,20 +696,20 @@ function wrapExpress(fn) { * @param obj the object (controller exports) * @returns {Object|Array} the wrapped object */ -function autoWrapExpress(obj) { +function autoWrapExpress (obj) { if (_.isArray(obj)) { - return obj.map(autoWrapExpress); + return obj.map(autoWrapExpress) } if (_.isFunction(obj)) { if (obj.constructor.name === 'AsyncFunction') { - return wrapExpress(obj); + return wrapExpress(obj) } - return obj; + return obj } _.each(obj, (value, key) => { - obj[key] = autoWrapExpress(value); - }); - return obj; + obj[key] = autoWrapExpress(value) + }) + return obj } /** @@ -711,11 +718,11 @@ function autoWrapExpress(obj) { * @param {Number} page the page number * @returns {String} link for the page */ -function getPageLink(req, page) { - const q = _.assignIn({}, req.query, { page }); +function getPageLink (req, page) { + const q = _.assignIn({}, req.query, { page }) return `${req.protocol}://${req.get('Host')}${req.baseUrl}${ req.path - }?${querystring.stringify(q)}`; + }?${querystring.stringify(q)}` } /** @@ -724,31 +731,31 @@ function getPageLink(req, page) { * @param {Object} res the HTTP response * @param {Object} result the operation result */ -function setResHeaders(req, res, result) { - const totalPages = Math.ceil(result.total / result.perPage); +function setResHeaders (req, res, result) { + const totalPages = Math.ceil(result.total / result.perPage) if (result.page > 1) { - res.set('X-Prev-Page', result.page - 1); + res.set('X-Prev-Page', result.page - 1) } if (result.page < totalPages) { - res.set('X-Next-Page', result.page + 1); + res.set('X-Next-Page', result.page + 1) } - res.set('X-Page', result.page); - res.set('X-Per-Page', result.perPage); - res.set('X-Total', result.total); - res.set('X-Total-Pages', totalPages); + res.set('X-Page', result.page) + res.set('X-Per-Page', result.perPage) + res.set('X-Total', result.total) + res.set('X-Total-Pages', totalPages) // set Link header if (totalPages > 0) { let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink( req, totalPages - )}>; rel="last"`; + )}>; rel="last"` if (result.page > 1) { - link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"`; + link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"` } if (result.page < totalPages) { - link += `, <${getPageLink(req, result.page + 1)}>; rel="next"`; + link += `, <${getPageLink(req, result.page + 1)}>; rel="next"` } - res.set('Link', link); + res.set('Link', link) } } @@ -756,30 +763,30 @@ function setResHeaders(req, res, result) { * Get ES Client * @return {Object} Elastic Host Client Instance */ -function getESClient() { +function getESClient () { if (esClients.client) { - return esClients.client; + return esClients.client } - const host = config.esConfig.HOST; - const cloudId = config.esConfig.ELASTICCLOUD.id; + const host = config.esConfig.HOST + const cloudId = config.esConfig.ELASTICCLOUD.id if (cloudId) { // Elastic Cloud configuration esClients.client = new elasticsearch.Client({ cloud: { - id: cloudId, + id: cloudId }, auth: { username: config.esConfig.ELASTICCLOUD.username, - password: config.esConfig.ELASTICCLOUD.password, - }, - }); + password: config.esConfig.ELASTICCLOUD.password + } + }) } else { esClients.client = new elasticsearch.Client({ - node: host, - }); + node: host + }) } - return esClients.client; + return esClients.client } /* @@ -790,8 +797,8 @@ const getM2MToken = async () => { return await m2m.getMachineToken( config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET - ); -}; + ) +} /* * Function to get M2M token for U-Bahn @@ -801,8 +808,8 @@ const getM2MUbahnToken = async () => { return await m2mForUbahn.getMachineToken( config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET - ); -}; + ) +} /** * Function to encode query string @@ -810,17 +817,17 @@ const getM2MUbahnToken = async () => { * @param {String} nesting the nesting string * @returns {String} query string */ -function encodeQueryString(queryObj, nesting = '') { +function encodeQueryString (queryObj, nesting = '') { const pairs = Object.entries(queryObj).map(([key, val]) => { // Handle the nested, recursive case, where the value to encode is an object itself if (typeof val === 'object') { - return encodeQueryString(val, nesting + `${key}.`); + return encodeQueryString(val, nesting + `${key}.`) } else { // Handle base case, where the value to encode is simply a string. - return [nesting + key, val].map(querystring.escape).join('='); + return [nesting + key, val].map(querystring.escape).join('=') } - }); - return pairs.join('&'); + }) + return pairs.join('&') } /** @@ -828,31 +835,31 @@ function encodeQueryString(queryObj, nesting = '') { * @param {Integer} externalId the legacy user id * @returns {Array} the users found */ -async function listUsersByExternalId(externalId) { +async function listUsersByExternalId (externalId) { // return empty list if externalId is null or undefined if (!!externalId !== true) { - return []; + return [] } - const token = await getM2MUbahnToken(); + const token = await getM2MUbahnToken() const q = { enrich: true, externalProfile: { organizationId: config.ORG_ID, - externalId, - }, - }; - const url = `${config.TC_API}/users?${encodeQueryString(q)}`; + externalId + } + } + const url = `${config.TC_API}/users?${encodeQueryString(q)}` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listUserByExternalId', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -860,14 +867,14 @@ async function listUsersByExternalId(externalId) { * @param {Integer} externalId the legacy user id * @returns {Object} the user */ -async function getUserByExternalId(externalId) { - const users = await listUsersByExternalId(externalId); +async function getUserByExternalId (externalId) { + const users = await listUsersByExternalId(externalId) if (_.isEmpty(users)) { throw new errors.NotFoundError( `externalId: ${externalId} "user" not found` - ); + ) } - return users[0]; + return users[0] } /** @@ -876,24 +883,24 @@ async function getUserByExternalId(externalId) { * @params {Object} payload the payload * @params {Object} options the extra options to control the function */ -async function postEvent(topic, payload, options = {}) { +async function postEvent (topic, payload, options = {}) { logger.debug({ component: 'helper', context: 'postEvent', message: `Posting event to Kafka topic ${topic}, ${JSON.stringify( payload - )}`, - }); - const client = getBusApiClient(); + )}` + }) + const client = getBusApiClient() const message = { topic, originator: config.KAFKA_MESSAGE_ORIGINATOR, timestamp: new Date().toISOString(), 'mime-type': 'application/json', - payload, - }; - await client.postEvent(message); - await eventDispatcher.handleEvent(topic, { value: payload, options }); + payload + } + await client.postEvent(message) + await eventDispatcher.handleEvent(topic, { value: payload, options }) } /** @@ -902,11 +909,11 @@ async function postEvent(topic, payload, options = {}) { * @param {Object} err the err * @returns {Boolean} the result */ -function isDocumentMissingException(err) { +function isDocumentMissingException (err) { if (err.statusCode === 404 && err instanceof ESResponseError) { - return true; + return true } - return false; + return false } /** @@ -915,34 +922,34 @@ function isDocumentMissingException(err) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getProjects(currentUser, criteria = {}) { - let token; +async function getProjects (currentUser, criteria = {}) { + let token if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken(); - token = `Bearer ${m2mToken}`; + const m2mToken = await getM2MToken() + token = `Bearer ${m2mToken}` } else { - token = currentUser.jwtToken; + token = currentUser.jwtToken } - const url = `${config.TC_API}/projects?type=talent-as-a-service`; + const url = `${config.TC_API}/projects?type=talent-as-a-service` const res = await request .get(url) .query(criteria) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getProjects', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) const result = _.map(res.body, (item) => { - return _.pick(item, ['id', 'name', 'invites', 'members']); - }); + return _.pick(item, ['id', 'name', 'invites', 'members']) + }) return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result, - }; + result + } } /** @@ -951,24 +958,24 @@ async function getProjects(currentUser, criteria = {}) { * @param {String} userId the legacy user id * @returns {Object} the user */ -async function getTopcoderUserById(userId) { - const token = await getM2MToken(); +async function getTopcoderUserById (userId) { + const token = await getM2MToken() const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `id=${userId}` }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getTopcoderUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - const user = _.get(res.body, 'result.content[0]'); + message: `response body: ${JSON.stringify(res.body)}` + }) + const user = _.get(res.body, 'result.content[0]') if (!user) { throw new errors.NotFoundError( `userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}` - ); + ) } - return user; + return user } /** @@ -976,31 +983,31 @@ async function getTopcoderUserById(userId) { * @param {String} userId the user id * @returns the request result */ -async function getUserById(userId, enrich) { - const token = await getM2MUbahnToken(); +async function getUserById (userId, enrich) { + const token = await getM2MUbahnToken() const res = await request .get(`${config.TC_API}/users/${userId}` + (enrich ? '?enrich=true' : '')) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) - const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']); + const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']) if (enrich) { user.skills = (res.body.skills || []).map((skillObj) => _.pick(skillObj.skill, ['id', 'name']) - ); - const attributes = _.get(res, 'body.attributes', []); + ) + const attributes = _.get(res, 'body.attributes', []) user.attributes = _.map(attributes, (attr) => _.pick(attr, ['id', 'value', 'attribute.id', 'attribute.name']) - ); + ) } - return user; + return user } /** @@ -1008,19 +1015,19 @@ async function getUserById(userId, enrich) { * @param {Object} data the user data * @returns the request result */ -async function createUbahnUser({ handle, firstName, lastName }) { - const token = await getM2MUbahnToken(); +async function createUbahnUser ({ handle, firstName, lastName }) { + const token = await getM2MUbahnToken() const res = await request .post(`${config.TC_API}/users`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ handle, firstName, lastName }); + .send({ handle, firstName, lastName }) localLogger.debug({ context: 'createUbahnUser', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id']) } /** @@ -1028,21 +1035,21 @@ async function createUbahnUser({ handle, firstName, lastName }) { * @param {String} userId the user id(with uuid format) * @param {Object} data the profile data */ -async function createUserExternalProfile( +async function createUserExternalProfile ( userId, { organizationId, externalId } ) { - const token = await getM2MUbahnToken(); + const token = await getM2MUbahnToken() const res = await request .post(`${config.TC_API}/users/${userId}/externalProfiles`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ organizationId, externalId: String(externalId) }); + .send({ organizationId, externalId: String(externalId) }) localLogger.debug({ context: 'createUserExternalProfile', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) } /** @@ -1050,23 +1057,23 @@ async function createUserExternalProfile( * @param {Array} handles the handle array * @returns the request result */ -async function getMembers(handles) { - const token = await getM2MToken(); +async function getMembers (handles) { + const token = await getM2MToken() const handlesStr = _.map(handles, (handle) => { - return '%22' + handle.toLowerCase() + '%22'; - }).join(','); - const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]`; + return '%22' + handle.toLowerCase() + '%22' + }).join(',') + const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getMembers', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -1075,36 +1082,36 @@ async function getMembers(handles) { * @param {Number} id project id * @returns the request result */ -async function getProjectById(currentUser, id) { - let token; +async function getProjectById (currentUser, id) { + let token if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken(); - token = `Bearer ${m2mToken}`; + const m2mToken = await getM2MToken() + token = `Bearer ${m2mToken}` } else { - token = currentUser.jwtToken; + token = currentUser.jwtToken } - const url = `${config.TC_API}/projects/${id}`; + const url = `${config.TC_API}/projects/${id}` try { const res = await request .get(url) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getProjectById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id', 'name', 'invites', 'members']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id', 'name', 'invites', 'members']) } catch (err) { if (err.status === HttpStatus.FORBIDDEN) { throw new errors.ForbiddenError( `You are not allowed to access the project with id ${id}` - ); + ) } if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${id} project not found`); + throw new errors.NotFoundError(`id: ${id} project not found`) } - throw err; + throw err } } @@ -1115,33 +1122,33 @@ async function getProjectById(currentUser, id) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getTopcoderSkills(criteria) { - const token = await getM2MUbahnToken(); +async function getTopcoderSkills (criteria) { + const token = await getM2MUbahnToken() try { const res = await request .get(`${config.TC_API}/skills`) .query({ skillProviderId: config.TOPCODER_SKILL_PROVIDER_ID, - ...criteria, + ...criteria }) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getTopcoderSkills', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result: res.body, - }; + result: res.body + } } catch (err) { if (err.status === HttpStatus.BAD_REQUEST) { - throw new errors.BadRequestError(err.response.body.message); + throw new errors.BadRequestError(err.response.body.message) } - throw err; + throw err } } @@ -1150,18 +1157,18 @@ async function getTopcoderSkills(criteria) { * @param {String} skillId the skill Id * @returns the request result */ -async function getSkillById(skillId) { - const token = await getM2MUbahnToken(); +async function getSkillById (skillId) { + const token = await getM2MUbahnToken() const res = await request .get(`${config.TC_API}/skills/${skillId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getSkillById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id', 'name']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id', 'name']) } /** @@ -1174,22 +1181,22 @@ async function getSkillById(skillId) { * @params {Object} currentUser the user who perform this operation * @returns {String} the ubahn user id */ -async function ensureUbahnUserId(currentUser) { +async function ensureUbahnUserId (currentUser) { try { - return (await getUserByExternalId(currentUser.userId)).id; + return (await getUserByExternalId(currentUser.userId)).id } catch (err) { if (!(err instanceof errors.NotFoundError)) { - throw err; + throw err } - const topcoderUser = await getTopcoderUserById(currentUser.userId); + const topcoderUser = await getTopcoderUserById(currentUser.userId) const user = await createUbahnUser( _.pick(topcoderUser, ['handle', 'firstName', 'lastName']) - ); + ) await createUserExternalProfile(user.id, { organizationId: config.ORG_ID, - externalId: currentUser.userId, - }); - return user.id; + externalId: currentUser.userId + }) + return user.id } } @@ -1199,8 +1206,8 @@ async function ensureUbahnUserId(currentUser) { * @param {String} jobId the job id * @returns {Object} the job data */ -async function ensureJobById(jobId) { - return models.Job.findById(jobId); +async function ensureJobById (jobId) { + return models.Job.findById(jobId) } /** @@ -1209,8 +1216,8 @@ async function ensureJobById(jobId) { * @param {String} resourceBookingId the resourceBooking id * @returns {Object} the resourceBooking data */ -async function ensureResourceBookingById(resourceBookingId) { - return models.ResourceBooking.findById(resourceBookingId); +async function ensureResourceBookingById (resourceBookingId) { + return models.ResourceBooking.findById(resourceBookingId) } /** @@ -1218,8 +1225,8 @@ async function ensureResourceBookingById(resourceBookingId) { * @param {String} workPeriodId the workPeriod id * @returns the workPeriod data */ -async function ensureWorkPeriodById(workPeriodId) { - return models.WorkPeriod.findById(workPeriodId); +async function ensureWorkPeriodById (workPeriodId) { + return models.WorkPeriod.findById(workPeriodId) } /** @@ -1228,24 +1235,24 @@ async function ensureWorkPeriodById(workPeriodId) { * @param {String} jobId the user id * @returns {Object} the user data */ -async function ensureUserById(userId) { - const token = await getM2MUbahnToken(); +async function ensureUserById (userId) { + const token = await getM2MUbahnToken() try { const res = await request .get(`${config.TC_API}/users/${userId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'ensureUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${userId} "user" not found`); + throw new errors.NotFoundError(`id: ${userId} "user" not found`) } - throw err; + throw err } } @@ -1254,12 +1261,12 @@ async function ensureUserById(userId) { * * @returns {Object} the M2M auth user */ -function getAuditM2Muser() { +function getAuditM2Muser () { return { isMachine: true, userId: config.m2m.M2M_AUDIT_USER_ID, - handle: config.m2m.M2M_AUDIT_HANDLE, - }; + handle: config.m2m.M2M_AUDIT_HANDLE + } } /** @@ -1271,24 +1278,24 @@ function getAuditM2Muser() { * @param {Number} projectId project id * @returns the result */ -async function checkIsMemberOfProject(userId, projectId) { - const m2mToken = await getM2MToken(); +async function checkIsMemberOfProject (userId, projectId) { + const m2mToken = await getM2MToken() const res = await request .get(`${config.TC_API}/projects/${projectId}`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const memberIdList = _.map(res.body.members, 'userId'); + .set('Accept', 'application/json') + const memberIdList = _.map(res.body.members, 'userId') localLogger.debug({ context: 'checkIsMemberOfProject', message: `the members of project ${projectId}: ${JSON.stringify( memberIdList - )}, authUserId: ${JSON.stringify(userId)}`, - }); + )}, authUserId: ${JSON.stringify(userId)}` + }) if (!memberIdList.includes(userId)) { throw new errors.UnauthorizedError( `userId: ${userId} the user is not a member of project ${projectId}` - ); + ) } } @@ -1298,24 +1305,24 @@ async function checkIsMemberOfProject(userId, projectId) { * @param {Array} handles the array of handles * @returns {Array} the member details */ -async function getMemberDetailsByHandles(handles) { +async function getMemberDetailsByHandles (handles) { if (!handles.length) { - return []; + return [] } - const token = await getM2MToken(); + const token = await getM2MToken() const res = await request .get(`${config.TOPCODER_MEMBERS_API}/`) .query({ 'handlesLower[]': handles.map(handle => handle.toLowerCase()), - fields: 'userId,handle,handleLower,firstName,lastName,email', + fields: 'userId,handle,handleLower,firstName,lastName,email' }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getMemberDetailsByHandles', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -1324,7 +1331,7 @@ async function getMemberDetailsByHandles(handles) { * @param {String} handle the user handle * @returns {Object} the member details */ -async function getMemberDetailsByHandle(handle) { +async function getMemberDetailsByHandle (handle) { const [memberDetails] = await getMemberDetailsByHandles([handle]) if (!memberDetails) { @@ -1341,20 +1348,20 @@ async function getMemberDetailsByHandle(handle) { * @param {String} email the email * @returns {Array} the member details */ -async function _getMemberDetailsByEmail(token, email) { +async function _getMemberDetailsByEmail (token, email) { const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `email=${email}`, - fields: 'handle,id,email,firstName,lastName', + fields: 'handle,id,email,firstName,lastName' }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: '_getMemberDetailsByEmail', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res.body, 'result.content'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res.body, 'result.content') } /** @@ -1364,25 +1371,25 @@ async function _getMemberDetailsByEmail(token, email) { * @param {Array} emails the array of emails * @returns {Array} the member details */ -async function getMemberDetailsByEmails(emails) { - const token = await getM2MToken(); +async function getMemberDetailsByEmails (emails) { + const token = await getM2MToken() const limiter = new Bottleneck({ - maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API, - }); + maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API + }) const membersArray = await Promise.all( emails.map((email) => limiter.schedule(() => _getMemberDetailsByEmail(token, email).catch((error) => { localLogger.error({ context: 'getMemberDetailsByEmails', - message: error.message, - }); - return []; + message: error.message + }) + return [] }) ) ) - ); - return _.flatten(membersArray); + ) + return _.flatten(membersArray) } /** @@ -1393,20 +1400,20 @@ async function getMemberDetailsByEmails(emails) { * @param {Object} criteria the filtering criteria * @returns {Object} the member created */ -async function createProjectMember(projectId, data, criteria) { - const m2mToken = await getM2MToken(); +async function createProjectMember (projectId, data, criteria) { + const m2mToken = await getM2MToken() const { body: member } = await request .post(`${config.TC_API}/projects/${projectId}/members`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') .query(criteria) - .send(data); + .send(data) localLogger.debug({ context: 'createProjectMember', - message: `response body: ${JSON.stringify(member)}`, - }); - return member; + message: `response body: ${JSON.stringify(member)}` + }) + return member } /** @@ -1416,21 +1423,21 @@ async function createProjectMember(projectId, data, criteria) { * @param {Object} criteria the search criteria * @returns {Array} the project members */ -async function listProjectMembers(currentUser, projectId, criteria = {}) { +async function listProjectMembers (currentUser, projectId, criteria = {}) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken const { body: members } = await request .get(`${config.TC_API}/projects/${projectId}/members`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listProjectMembers', - message: `response body: ${JSON.stringify(members)}`, - }); - return members; + message: `response body: ${JSON.stringify(members)}` + }) + return members } /** @@ -1440,21 +1447,21 @@ async function listProjectMembers(currentUser, projectId, criteria = {}) { * @param {Object} criteria the search criteria * @returns {Array} the member invites */ -async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { +async function listProjectMemberInvites (currentUser, projectId, criteria = {}) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken const { body: invites } = await request .get(`${config.TC_API}/projects/${projectId}/invites`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listProjectMemberInvites', - message: `response body: ${JSON.stringify(invites)}`, - }); - return invites; + message: `response body: ${JSON.stringify(invites)}` + }) + return invites } /** @@ -1464,24 +1471,24 @@ async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { * @param {String} projectMemberId the id of the project member * @returns {undefined} */ -async function deleteProjectMember(currentUser, projectId, projectMemberId) { +async function deleteProjectMember (currentUser, projectId, projectMemberId) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken try { await request .delete( `${config.TC_API}/projects/${projectId}/members/${projectMemberId}` ) - .set('Authorization', token); + .set('Authorization', token) } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { throw new errors.NotFoundError( `projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}` - ); + ) } - throw err; + throw err } } @@ -1491,13 +1498,13 @@ async function deleteProjectMember(currentUser, projectId, projectMemberId) { * @param {String} attributeName Requested attribute name, e.g. "email" * @returns attribute value */ -function getUserAttributeValue(user, attributeName) { - const attributes = _.get(user, 'attributes', []); +function getUserAttributeValue (user, attributeName) { + const attributes = _.get(user, 'attributes', []) const targetAttribute = _.find( attributes, (a) => a.attribute.name === attributeName - ); - return _.get(targetAttribute, 'value'); + ) + return _.get(targetAttribute, 'value') } /** @@ -1507,34 +1514,34 @@ function getUserAttributeValue(user, attributeName) { * @param {String} token m2m token * @returns {Object} the challenge created */ -async function createChallenge(data, token) { +async function createChallenge (data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/challenges`; + const url = `${config.TC_API}/challenges` localLogger.debug({ context: 'createChallenge', - message: `EndPoint: POST ${url}`, - }); + message: `EndPoint: POST ${url}` + }) localLogger.debug({ context: 'createChallenge', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: challenge, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createChallenge', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'createChallenge', - message: `Response Body: ${JSON.stringify(challenge)}`, - }); - return challenge; + message: `Response Body: ${JSON.stringify(challenge)}` + }) + return challenge } /** @@ -1545,34 +1552,34 @@ async function createChallenge(data, token) { * @param {String} token m2m token * @returns {Object} the challenge updated */ -async function updateChallenge(challengeId, data, token) { +async function updateChallenge (challengeId, data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/challenges/${challengeId}`; + const url = `${config.TC_API}/challenges/${challengeId}` localLogger.debug({ context: 'updateChallenge', - message: `EndPoint: PATCH ${url}`, - }); + message: `EndPoint: PATCH ${url}` + }) localLogger.debug({ context: 'updateChallenge', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: challenge, status: httpStatus } = await request .patch(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'updateChallenge', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'updateChallenge', - message: `Response Body: ${JSON.stringify(challenge)}`, - }); - return challenge; + message: `Response Body: ${JSON.stringify(challenge)}` + }) + return challenge } /** @@ -1582,34 +1589,34 @@ async function updateChallenge(challengeId, data, token) { * @param {String} token m2m token * @returns {Object} the resource created */ -async function createChallengeResource(data, token) { +async function createChallengeResource (data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/resources`; + const url = `${config.TC_API}/resources` localLogger.debug({ context: 'createChallengeResource', - message: `EndPoint: POST ${url}`, - }); + message: `EndPoint: POST ${url}` + }) localLogger.debug({ context: 'createChallengeResource', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: resource, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createChallengeResource', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'createChallengeResource', - message: `Response Body: ${JSON.stringify(resource)}`, - }); - return resource; + message: `Response Body: ${JSON.stringify(resource)}` + }) + return resource } /** @@ -1618,40 +1625,40 @@ async function createChallengeResource(data, token) { * @param {Date} end end date of the resource booking * @returns {Array<{startDate:Date, endDate:Date, daysWorked:number}>} information about workPeriods */ -function extractWorkPeriods(start, end) { +function extractWorkPeriods (start, end) { // calculate maximum possible daysWorked for a week - function getDaysWorked(week) { + function getDaysWorked (week) { if (weeks === 1) { - return Math.min(endDay, 5) - Math.max(startDay, 1) + 1; + return Math.min(endDay, 5) - Math.max(startDay, 1) + 1 } else if (week === 0) { - return Math.min(6 - startDay, 5); + return Math.min(6 - startDay, 5) } else if (week === weeks - 1) { - return Math.min(endDay, 5); - } else return 5; + return Math.min(endDay, 5) + } else return 5 } - const periods = []; + const periods = [] if (_.isNil(start) || _.isNil(end)) { - return periods; + return periods } - const startDate = moment(start); - const startDay = startDate.get('day'); - startDate.set('day', 0).startOf('day'); + const startDate = moment(start) + const startDay = startDate.get('day') + startDate.set('day', 0).startOf('day') - const endDate = moment(end); - const endDay = endDate.get('day'); - endDate.set('day', 6).endOf('day'); + const endDate = moment(end) + const endDay = endDate.get('day') + endDate.set('day', 6).endOf('day') - const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7; + const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7 for (let i = 0; i < weeks; i++) { periods.push({ startDate: startDate.format('YYYY-MM-DD'), endDate: startDate.add(6, 'day').format('YYYY-MM-DD'), - daysWorked: getDaysWorked(i), - }); - startDate.add(1, 'day'); + daysWorked: getDaysWorked(i) + }) + startDate.add(1, 'day') } - return periods; + return periods } /** @@ -1660,19 +1667,19 @@ function extractWorkPeriods(start, end) { * @param {String} userHandle user handle * @returns {String} email address of the user */ -async function getUserByHandle(userHandle) { - const token = await getM2MToken(); - const url = `${config.TC_API}/members/${userHandle}`; +async function getUserByHandle (userHandle) { + const token = await getM2MToken() + const url = `${config.TC_API}/members/${userHandle}` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getUserByHandle', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res, 'body'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res, 'body') } /** @@ -1681,14 +1688,14 @@ async function getUserByHandle(userHandle) { * @param {*} object of json that would be replaced in string * @returns */ -async function substituteStringByObject(string, object) { +async function substituteStringByObject (string, object) { for (var key in object) { if (!Object.prototype.hasOwnProperty.call(object, key)) { - continue; + continue } - string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]); + string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]) } - return string; + return string } /** @@ -1696,19 +1703,19 @@ async function substituteStringByObject(string, object) { * @param {Object} data title of project and any other info * @returns {Object} the project created */ -async function createProject(currentUser, data) { - const token = currentUser.jwtToken; +async function createProject (currentUser, data) { + const token = currentUser.jwtToken const res = await request .post(`${config.TC_API}/projects/`) .set('Authorization', token) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createProject', - message: `response body: ${JSON.stringify(res)}`, - }); - return _.get(res, 'body'); + message: `response body: ${JSON.stringify(res)}` + }) + return _.get(res, 'body') } module.exports = { @@ -1727,9 +1734,9 @@ module.exports = { getUserId: async (userId) => { // check m2m user id if (userId === config.m2m.M2M_AUDIT_USER_ID) { - return config.m2m.M2M_AUDIT_USER_ID; + return config.m2m.M2M_AUDIT_USER_ID } - return ensureUbahnUserId({ userId }); + return ensureUbahnUserId({ userId }) }, getUserByExternalId, getM2MToken, @@ -1763,5 +1770,5 @@ module.exports = { extractWorkPeriods, getUserByHandle, substituteStringByObject, - createProject, -}; + createProject +} diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index ca4f1bca..26d70738 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -1,19 +1,19 @@ /** * Controller for TaaS teams endpoints */ -const HttpStatus = require('http-status-codes'); -const service = require('../services/TeamService'); -const helper = require('../common/helper'); +const HttpStatus = require('http-status-codes') +const service = require('../services/TeamService') +const helper = require('../common/helper') /** * Search teams * @param req the request * @param res the response */ -async function searchTeams(req, res) { - const result = await service.searchTeams(req.authUser, req.query); - helper.setResHeaders(req, res, result); - res.send(result.result); +async function searchTeams (req, res) { + const result = await service.searchTeams(req.authUser, req.query) + helper.setResHeaders(req, res, result) + res.send(result.result) } /** @@ -21,8 +21,8 @@ async function searchTeams(req, res) { * @param req the request * @param res the response */ -async function getTeam(req, res) { - res.send(await service.getTeam(req.authUser, req.params.id)); +async function getTeam (req, res) { + res.send(await service.getTeam(req.authUser, req.params.id)) } /** @@ -30,10 +30,10 @@ async function getTeam(req, res) { * @param req the request * @param res the response */ -async function getTeamJob(req, res) { +async function getTeamJob (req, res) { res.send( await service.getTeamJob(req.authUser, req.params.id, req.params.jobId) - ); + ) } /** @@ -41,9 +41,9 @@ async function getTeamJob(req, res) { * @param req the request * @param res the response */ -async function sendEmail(req, res) { - await service.sendEmail(req.authUser, req.body); - res.status(HttpStatus.NO_CONTENT).end(); +async function sendEmail (req, res) { + await service.sendEmail(req.authUser, req.body) + res.status(HttpStatus.NO_CONTENT).end() } /** @@ -51,10 +51,10 @@ async function sendEmail(req, res) { * @param req the request * @param res the response */ -async function addMembers(req, res) { +async function addMembers (req, res) { res.send( await service.addMembers(req.authUser, req.params.id, req.query, req.body) - ); + ) } /** @@ -62,13 +62,13 @@ async function addMembers(req, res) { * @param req the request * @param res the response */ -async function searchMembers(req, res) { +async function searchMembers (req, res) { const result = await service.searchMembers( req.authUser, req.params.id, req.query - ); - res.send(result.result); + ) + res.send(result.result) } /** @@ -76,13 +76,13 @@ async function searchMembers(req, res) { * @param req the request * @param res the response */ -async function searchInvites(req, res) { +async function searchInvites (req, res) { const result = await service.searchInvites( req.authUser, req.params.id, req.query - ); - res.send(result.result); + ) + res.send(result.result) } /** @@ -90,13 +90,13 @@ async function searchInvites(req, res) { * @param req the request * @param res the response */ -async function deleteMember(req, res) { +async function deleteMember (req, res) { await service.deleteMember( req.authUser, req.params.id, req.params.projectMemberId - ); - res.status(HttpStatus.NO_CONTENT).end(); + ) + res.status(HttpStatus.NO_CONTENT).end() } /** @@ -104,8 +104,8 @@ async function deleteMember(req, res) { * @param req the request * @param res the response */ -async function getMe(req, res) { - res.send(await service.getMe(req.authUser)); +async function getMe (req, res) { + res.send(await service.getMe(req.authUser)) } /** @@ -113,8 +113,8 @@ async function getMe(req, res) { * @param req the request * @param res the response */ -async function createProj(req, res) { - res.send(await service.createProj(req.authUser, req.body)); +async function createProj (req, res) { + res.send(await service.createProj(req.authUser, req.body)) } module.exports = { @@ -127,5 +127,5 @@ module.exports = { searchInvites, deleteMember, getMe, - createProj, -}; + createProj +} diff --git a/src/models/Job.js b/src/models/Job.js index 49d34ff7..477327f6 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -104,6 +104,36 @@ module.exports = (sequelize) => { defaultValue: false, allowNull: false }, + minSalary: { + field: 'min_salary', + type: Sequelize.INTEGER, + allowNull: false + }, + maxSalary: { + field: 'max_salary', + type: Sequelize.INTEGER, + allowNull: false + }, + hoursPerWeek: { + field: 'hours_per_week', + type: Sequelize.INTEGER, + allowNull: false + }, + jobLocation: { + field: 'job_location', + type: Sequelize.STRING(255), + allowNull: false + }, + jobTimezone: { + field: 'job_timezone', + type: Sequelize.STRING(128), + allowNull: false + }, + currency: { + field: 'currency', + type: Sequelize.STRING(30), + allowNull: false + }, createdBy: { field: 'created_by', type: Sequelize.UUID, diff --git a/src/models/JobCandidate.js b/src/models/JobCandidate.js index 819b9177..fc54c0aa 100644 --- a/src/models/JobCandidate.js +++ b/src/models/JobCandidate.js @@ -62,6 +62,9 @@ module.exports = (sequelize) => { resume: { type: Sequelize.STRING(2048) }, + remark: { + type: Sequelize.STRING(255) + }, createdBy: { field: 'created_by', type: Sequelize.UUID, diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index 9bbe25c6..07d777d4 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -1,7 +1,7 @@ /** * Contains taas team routes */ -const constants = require('../../app-constants'); +const constants = require('../../app-constants') module.exports = { '/taas-teams': { @@ -9,85 +9,85 @@ module.exports = { controller: 'TeamController', method: 'searchTeams', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/email': { post: { controller: 'TeamController', method: 'sendEmail', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/skills': { get: { controller: 'SkillController', method: 'searchSkills', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/me': { get: { controller: 'TeamController', method: 'getMe', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id': { get: { controller: 'TeamController', method: 'getTeam', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/jobs/:jobId': { get: { controller: 'TeamController', method: 'getTeamJob', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/members': { post: { controller: 'TeamController', method: 'addMembers', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], + scopes: [constants.Scopes.READ_TAAS_TEAM] }, get: { controller: 'TeamController', method: 'searchMembers', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/invites': { get: { controller: 'TeamController', method: 'searchInvites', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/members/:projectMemberId': { delete: { controller: 'TeamController', method: 'deleteMember', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/createTeamRequest': { post: { controller: 'TeamController', method: 'createProj', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, - }, -}; + scopes: [constants.Scopes.READ_TAAS_TEAM] + } + } +} diff --git a/src/services/InterviewService.js b/src/services/InterviewService.js index 10a065f4..a69a788c 100644 --- a/src/services/InterviewService.js +++ b/src/services/InterviewService.js @@ -241,8 +241,8 @@ async function requestInterview (currentUser, jobCandidateId, interview) { const guestMembers = await helper.getMemberDetailsByEmails(interview.guestEmails) interview.hostName = `${hostMembers[0].firstName} ${hostMembers[0].lastName}` interview.guestNames = _.map(interview.guestEmails, (guestEmail) => { - var foundGuestMember = _.find(guestMembers, function(guestMember) { return guestEmail == guestMember.email }); - return (foundGuestMember != undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split("@")[0] + var foundGuestMember = _.find(guestMembers, function (guestMember) { return guestEmail === guestMember.email }) + return (foundGuestMember !== undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split('@')[0] }) try { diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index 392d0af6..cc059c0c 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -130,7 +130,8 @@ createJobCandidate.schema = Joi.object().keys({ jobId: Joi.string().uuid().required(), userId: Joi.string().uuid().required(), externalId: Joi.string().allow(null), - resume: Joi.string().uri().allow(null) + resume: Joi.string().uri().allow(null), + remark: Joi.string().allow(null) }).required() }).required() @@ -176,7 +177,8 @@ partiallyUpdateJobCandidate.schema = Joi.object().keys({ data: Joi.object().keys({ status: Joi.jobCandidateStatus(), externalId: Joi.string().allow(null), - resume: Joi.string().uri().allow(null) + resume: Joi.string().uri().allow(null), + remark: Joi.string().allow(null) }).required() }).required() @@ -201,7 +203,8 @@ fullyUpdateJobCandidate.schema = Joi.object().keys({ userId: Joi.string().uuid().required(), status: Joi.jobCandidateStatus().default('open'), externalId: Joi.string().allow(null).default(null), - resume: Joi.string().uri().allow(null).default(null) + resume: Joi.string().uri().allow(null).default(null), + remark: Joi.string().allow(null).default(null) }).required() }).required() diff --git a/src/services/JobService.js b/src/services/JobService.js index 61685901..09b48d71 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -177,7 +177,13 @@ createJob.schema = Joi.object().keys({ rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()).required(), - isApplicationPageActive: Joi.boolean() + isApplicationPageActive: Joi.boolean(), + minSalary: Joi.number().integer().required(), + maxSalary: Joi.number().integer().required(), + hoursPerWeek: Joi.number().integer().required(), + jobLocation: Joi.string().required(), + jobTimezone: Joi.string().required(), + currency: Joi.string().required() }).required() }).required() @@ -245,7 +251,13 @@ partiallyUpdateJob.schema = Joi.object().keys({ rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()), - isApplicationPageActive: Joi.boolean() + isApplicationPageActive: Joi.boolean(), + minSalary: Joi.number().integer(), + maxSalary: Joi.number().integer(), + hoursPerWeek: Joi.number().integer(), + jobLocation: Joi.string(), + jobTimezone: Joi.string(), + currency: Joi.string() }).required() }).required() @@ -276,7 +288,13 @@ fullyUpdateJob.schema = Joi.object().keys({ workload: Joi.workload().allow(null).default(null), skills: Joi.array().items(Joi.string().uuid()).required(), status: Joi.jobStatus().default('sourcing'), - isApplicationPageActive: Joi.boolean() + isApplicationPageActive: Joi.boolean(), + minSalary: Joi.number().integer().required(), + maxSalary: Joi.number().integer().required(), + hoursPerWeek: Joi.number().integer().required(), + jobLocation: Joi.string().required(), + jobTimezone: Joi.string().required(), + currency: Joi.string().required() }).required() }).required() diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index f5c40206..fd3d7773 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unreachable */ /** * This service provides operations of ResourceBooking. */ diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 3f6dbfd3..4052e942 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -2,16 +2,16 @@ * This service provides operations of Job. */ -const _ = require('lodash'); -const Joi = require('joi'); -const dateFNS = require('date-fns'); -const config = require('config'); -const emailTemplateConfig = require('../../config/email_template.config'); -const helper = require('../common/helper'); -const logger = require('../common/logger'); -const errors = require('../common/errors'); -const JobService = require('./JobService'); -const ResourceBookingService = require('./ResourceBookingService'); +const _ = require('lodash') +const Joi = require('joi') +const dateFNS = require('date-fns') +const config = require('config') +const emailTemplateConfig = require('../../config/email_template.config') +const helper = require('../common/helper') +const logger = require('../common/logger') +const errors = require('../common/errors') +const JobService = require('./JobService') +const ResourceBookingService = require('./ResourceBookingService') const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { return { @@ -20,9 +20,9 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { from: template.from, recipients: template.recipients, cc: template.cc, - sendgridTemplateId: template.sendgridTemplateId, - }; -}); + sendgridTemplateId: template.sendgridTemplateId + } +}) /** * Function to get placed resource bookings with specific projectIds @@ -30,14 +30,14 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { * @param {Array} projectIds project ids * @returns the request result */ -async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { - const criteria = { status: 'placed', projectIds }; +async function _getPlacedResourceBookingsByProjectIds (currentUser, projectIds) { + const criteria = { status: 'placed', projectIds } const { result } = await ResourceBookingService.searchResourceBookings( currentUser, criteria, { returnAll: true } - ); - return result; + ) + return result } /** @@ -46,13 +46,13 @@ async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { * @param {Array} projectIds project ids * @returns the request result */ -async function _getJobsByProjectIds(currentUser, projectIds) { +async function _getJobsByProjectIds (currentUser, projectIds) { const { result } = await JobService.searchJobs( currentUser, { projectIds }, { returnAll: true } - ); - return result; + ) + return result } /** @@ -61,26 +61,26 @@ async function _getJobsByProjectIds(currentUser, projectIds) { * @param {Object} criteria the search criteria * @returns {Object} the search result, contain total/page/perPage and result array */ -async function searchTeams(currentUser, criteria) { - const sort = `${criteria.sortBy} ${criteria.sortOrder}`; +async function searchTeams (currentUser, criteria) { + const sort = `${criteria.sortBy} ${criteria.sortOrder}` // Get projects from /v5/projects with searching criteria const { total, page, perPage, - result: projects, + result: projects } = await helper.getProjects(currentUser, { page: criteria.page, perPage: criteria.perPage, name: criteria.name, - sort, - }); + sort + }) return { total, page, perPage, - result: await getTeamDetail(currentUser, projects), - }; + result: await getTeamDetail(currentUser, projects) + } } searchTeams.schema = Joi.object() @@ -107,13 +107,13 @@ searchTeams.schema = Joi.object() then: Joi.forbidden().label( 'sortOrder(with sortBy being `best match`)' ), - otherwise: Joi.string().valid('asc', 'desc').default('desc'), + otherwise: Joi.string().valid('asc', 'desc').default('desc') }), - name: Joi.string(), + name: Joi.string() }) - .required(), + .required() }) - .required(); + .required() /** * Get team details @@ -122,69 +122,69 @@ searchTeams.schema = Joi.object() * @param {Object} isSearch the flag whether for search function * @returns {Object} the search result */ -async function getTeamDetail(currentUser, projects, isSearch = true) { - const projectIds = _.map(projects, 'id'); +async function getTeamDetail (currentUser, projects, isSearch = true) { + const projectIds = _.map(projects, 'id') // Get all placed resourceBookings filtered by projectIds const resourceBookings = await _getPlacedResourceBookingsByProjectIds( currentUser, projectIds - ); + ) // Get all jobs filtered by projectIds - const jobs = await _getJobsByProjectIds(currentUser, projectIds); + const jobs = await _getJobsByProjectIds(currentUser, projectIds) // Get first week day and last week day - const curr = new Date(); - const firstDay = dateFNS.startOfWeek(curr); - const lastDay = dateFNS.endOfWeek(curr); + const curr = new Date() + const firstDay = dateFNS.startOfWeek(curr) + const lastDay = dateFNS.endOfWeek(curr) logger.debug({ component: 'TeamService', context: 'getTeamDetail', - message: `week started: ${firstDay}, week ended: ${lastDay}`, - }); + message: `week started: ${firstDay}, week ended: ${lastDay}` + }) - const result = []; + const result = [] for (const project of projects) { - const rbs = _.filter(resourceBookings, { projectId: project.id }); - const res = _.clone(project); - res.weeklyCost = 0; - res.resources = []; + const rbs = _.filter(resourceBookings, { projectId: project.id }) + const res = _.clone(project) + res.weeklyCost = 0 + res.resources = [] if (rbs && rbs.length > 0) { // Get minimal start date and maximal end date - const startDates = []; - const endDates = []; + const startDates = [] + const endDates = [] for (const rbsItem of rbs) { if (rbsItem.startDate) { - startDates.push(new Date(rbsItem.startDate)); + startDates.push(new Date(rbsItem.startDate)) } if (rbsItem.endDate) { - endDates.push(new Date(rbsItem.endDate)); + endDates.push(new Date(rbsItem.endDate)) } } if (startDates && startDates.length > 0) { - res.startDate = _.min(startDates); + res.startDate = _.min(startDates) } if (endDates && endDates.length > 0) { - res.endDate = _.max(endDates); + res.endDate = _.max(endDates) } // Count weekly rate for (const item of rbs) { // ignore any resourceBooking that has customerRate missed if (!item.customerRate) { - continue; + continue } - const startDate = new Date(item.startDate); - const endDate = new Date(item.endDate); + const startDate = new Date(item.startDate) + const endDate = new Date(item.endDate) // normally startDate is smaller than endDate for a resourceBooking so not check if startDate < endDate if ( (!item.startDate || startDate < lastDay) && (!item.endDate || endDate > firstDay) ) { - res.weeklyCost += item.customerRate; + res.weeklyCost += item.customerRate } } @@ -194,48 +194,48 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { const resource = { id: rb.id, userId: user.id, - ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']), - }; + ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']) + } // If call function is not search, add jobId field if (!isSearch) { - resource.jobId = rb.jobId; - resource.customerRate = rb.customerRate; - resource.startDate = rb.startDate; - resource.endDate = rb.endDate; + resource.jobId = rb.jobId + resource.customerRate = rb.customerRate + resource.startDate = rb.startDate + resource.endDate = rb.endDate } - return resource; - }); + return resource + }) }) - ); + ) if (resourceInfos && resourceInfos.length > 0) { - res.resources = resourceInfos; + res.resources = resourceInfos - const userHandles = _.map(resourceInfos, 'handle'); + const userHandles = _.map(resourceInfos, 'handle') // Get user photo from /v5/members - const members = await helper.getMembers(userHandles); + const members = await helper.getMembers(userHandles) for (const item of res.resources) { const findMember = _.find(members, { - handleLower: item.handle.toLowerCase(), - }); + handleLower: item.handle.toLowerCase() + }) if (findMember && findMember.photoURL) { - item.photo_url = findMember.photoURL; + item.photo_url = findMember.photoURL } } } } - const jobsTmp = _.filter(jobs, { projectId: project.id }); + const jobsTmp = _.filter(jobs, { projectId: project.id }) if (jobsTmp && jobsTmp.length > 0) { if (isSearch) { // Count total positions - res.totalPositions = 0; + res.totalPositions = 0 for (const item of jobsTmp) { // only sum numPositions of jobs whose status is NOT cancelled or closed if (['cancelled', 'closed'].includes(item.status)) { - continue; + continue } - res.totalPositions += item.numPositions; + res.totalPositions += item.numPositions } } else { res.jobs = _.map(jobsTmp, (job) => { @@ -249,15 +249,15 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { 'skills', 'customerRate', 'status', - 'title', - ]); - }); + 'title' + ]) + }) } } - result.push(res); + result.push(res) } - return result; + return result } /** @@ -266,35 +266,35 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { * @param {String} id the job id * @returns {Object} the team */ -async function getTeam(currentUser, id) { - const project = await helper.getProjectById(currentUser, id); - const result = await getTeamDetail(currentUser, [project], false); - const teamDetail = result[0]; +async function getTeam (currentUser, id) { + const project = await helper.getProjectById(currentUser, id) + const result = await getTeamDetail(currentUser, [project], false) + const teamDetail = result[0] // add job skills for result - let jobSkills = []; + let jobSkills = [] if (teamDetail && teamDetail.jobs) { for (const job of teamDetail.jobs) { if (job.skills) { - const usersPromises = []; + const usersPromises = [] _.map(job.skills, (skillId) => { - usersPromises.push(helper.getSkillById(skillId)); - }); - jobSkills = await Promise.all(usersPromises); - job.skills = jobSkills; + usersPromises.push(helper.getSkillById(skillId)) + }) + jobSkills = await Promise.all(usersPromises) + job.skills = jobSkills } } } - return teamDetail; + return teamDetail } getTeam.schema = Joi.object() .keys({ currentUser: Joi.object().required(), - id: Joi.number().integer().required(), + id: Joi.number().integer().required() }) - .required(); + .required() /** * Get team job with id @@ -303,25 +303,25 @@ getTeam.schema = Joi.object() * @param {String} jobId the job id * @returns the team job */ -async function getTeamJob(currentUser, id, jobId) { - const project = await helper.getProjectById(currentUser, id); - const jobs = await _getJobsByProjectIds(currentUser, [project.id]); - const job = _.find(jobs, { id: jobId }); +async function getTeamJob (currentUser, id, jobId) { + const project = await helper.getProjectById(currentUser, id) + const jobs = await _getJobsByProjectIds(currentUser, [project.id]) + const job = _.find(jobs, { id: jobId }) if (!job) { throw new errors.NotFoundError( `id: ${jobId} "Job" with Team id ${id} doesn't exist` - ); + ) } const result = { id: job.id, - title: job.title, - }; + title: job.title + } if (job.skills) { result.skills = await Promise.all( _.map(job.skills, (skillId) => helper.getSkillById(skillId)) - ); + ) } // If the job has candidates, the following data for each candidate would be populated: @@ -336,12 +336,12 @@ async function getTeamJob(currentUser, id, jobId) { _.map(_.uniq(_.map(job.candidates, 'userId')), (userId) => helper.getUserById(userId, true) ) - ); - const userMap = _.groupBy(users, 'id'); + ) + const userMap = _.groupBy(users, 'id') // find photo URLs for users - const members = await helper.getMembers(_.map(users, 'handle')); - const photoURLMap = _.groupBy(members, 'handleLower'); + const members = await helper.getMembers(_.map(users, 'handle')) + const photoURLMap = _.groupBy(members, 'handleLower') result.candidates = _.map(job.candidates, (candidate) => { const candidateData = _.pick(candidate, [ @@ -349,33 +349,33 @@ async function getTeamJob(currentUser, id, jobId) { 'resume', 'userId', 'interviews', - 'id', - ]); - const userData = userMap[candidate.userId][0]; + 'id' + ]) + const userData = userMap[candidate.userId][0] // attach user data to the candidate Object.assign( candidateData, _.pick(userData, ['handle', 'firstName', 'lastName', 'skills']) - ); + ) // attach photo URL to the candidate - const handleLower = userData.handle.toLowerCase(); + const handleLower = userData.handle.toLowerCase() if (photoURLMap[handleLower]) { - candidateData.photo_url = photoURLMap[handleLower][0].photoURL; + candidateData.photo_url = photoURLMap[handleLower][0].photoURL } - return candidateData; - }); + return candidateData + }) } - return result; + return result } getTeamJob.schema = Joi.object() .keys({ currentUser: Joi.object().required(), id: Joi.number().integer().required(), - jobId: Joi.string().guid().required(), + jobId: Joi.string().guid().required() }) - .required(); + .required() /** * Send email through a particular template @@ -383,21 +383,21 @@ getTeamJob.schema = Joi.object() * @param {Object} data the email object * @returns {undefined} */ -async function sendEmail(currentUser, data) { - const template = emailTemplates[data.template]; - const dataCC = data.cc || []; - const templateCC = template.cc || []; - const dataRecipients = data.recipients || []; - const templateRecipients = template.recipients || []; +async function sendEmail (currentUser, data) { + const template = emailTemplates[data.template] + const dataCC = data.cc || [] + const templateCC = template.cc || [] + const dataRecipients = data.recipients || [] + const templateRecipients = template.recipients || [] const subjectBody = { subject: data.subject || template.subject, - body: data.body || template.body, - }; + body: data.body || template.body + } for (const key in subjectBody) { subjectBody[key] = await helper.substituteStringByObject( subjectBody[key], data.data - ); + ) } const emailData = { // override template if coming data already have the 'from' address @@ -407,9 +407,9 @@ async function sendEmail(currentUser, data) { cc: _.uniq([...dataCC, ...templateCC]), data: { ...data.data, ...subjectBody }, sendgrid_template_id: template.sendgridTemplateId, - version: 'v3', - }; - await helper.postEvent(config.EMAIL_TOPIC, emailData); + version: 'v3' + } + await helper.postEvent(config.EMAIL_TOPIC, emailData) } sendEmail.schema = Joi.object() @@ -423,11 +423,11 @@ sendEmail.schema = Joi.object() data: Joi.object().required(), from: Joi.string().email(), recipients: Joi.array().items(Joi.string().email()).allow(null), - cc: Joi.array().items(Joi.string().email()).allow(null), + cc: Joi.array().items(Joi.string().email()).allow(null) }) - .required(), + .required() }) - .required(); + .required() /** * Add a member to a team as customer. @@ -437,25 +437,25 @@ sendEmail.schema = Joi.object() * @param {String} fields the fields to be returned * @returns {Object} the member added */ -async function _addMemberToProjectAsCustomer(projectId, userId, fields) { +async function _addMemberToProjectAsCustomer (projectId, userId, fields) { try { const member = await helper.createProjectMember( projectId, { userId: userId, role: 'customer' }, { fields } - ); - return member; + ) + return member } catch (err) { - err.message = _.get(err, 'response.body.message') || err.message; + err.message = _.get(err, 'response.body.message') || err.message if (err.message && err.message.includes('User already registered')) { - throw new Error('User is already added'); + throw new Error('User is already added') } logger.error({ component: 'TeamService', context: '_addMemberToProjectAsCustomer', - message: err.message, - }); - throw err; + message: err.message + }) + throw err } } @@ -467,16 +467,16 @@ async function _addMemberToProjectAsCustomer(projectId, userId, fields) { * @param {Object} data the object including members with handle/email to be added * @returns {Object} the success/failed added members */ -async function addMembers(currentUser, id, criteria, data) { - await helper.getProjectById(currentUser, id); // check whether the user can access the project +async function addMembers (currentUser, id, criteria, data) { + await helper.getProjectById(currentUser, id) // check whether the user can access the project const result = { success: [], - failed: [], - }; + failed: [] + } - const handles = data.handles || []; - const emails = data.emails || []; + const handles = data.handles || [] + const emails = data.emails || [] const handleMembers = await helper .getMemberDetailsByHandles(handles) @@ -484,9 +484,9 @@ async function addMembers(currentUser, id, criteria, data) { _.map(members, (member) => ({ ...member, // populate members with lower-cased handle for case insensitive search - handleLowerCase: member.handle.toLowerCase(), + handleLowerCase: member.handle.toLowerCase() })) - ); + ) const emailMembers = await helper .getMemberDetailsByEmails(emails) @@ -494,20 +494,20 @@ async function addMembers(currentUser, id, criteria, data) { _.map(members, (member) => ({ ...member, // populate members with lower-cased email for case insensitive search - emailLowerCase: member.email.toLowerCase(), + emailLowerCase: member.email.toLowerCase() })) - ); + ) await Promise.all([ Promise.all( handles.map((handle) => { const memberDetails = _.find(handleMembers, { - handleLowerCase: handle.toLowerCase(), - }); + handleLowerCase: handle.toLowerCase() + }) if (!memberDetails) { - result.failed.push({ error: "User doesn't exist", handle }); - return; + result.failed.push({ error: "User doesn't exist", handle }) + return } return _addMemberToProjectAsCustomer( @@ -517,23 +517,23 @@ async function addMembers(currentUser, id, criteria, data) { ) .then((member) => { // note, that we return `handle` in the same case it was in request - result.success.push({ ...member, handle }); + result.success.push({ ...member, handle }) }) .catch((err) => { - result.failed.push({ error: err.message, handle }); - }); + result.failed.push({ error: err.message, handle }) + }) }) ), Promise.all( emails.map((email) => { const memberDetails = _.find(emailMembers, { - emailLowerCase: email.toLowerCase(), - }); + emailLowerCase: email.toLowerCase() + }) if (!memberDetails) { - result.failed.push({ error: "User doesn't exist", email }); - return; + result.failed.push({ error: "User doesn't exist", email }) + return } return _addMemberToProjectAsCustomer( @@ -543,16 +543,16 @@ async function addMembers(currentUser, id, criteria, data) { ) .then((member) => { // note, that we return `email` in the same case it was in request - result.success.push({ ...member, email }); + result.success.push({ ...member, email }) }) .catch((err) => { - result.failed.push({ error: err.message, email }); - }); + result.failed.push({ error: err.message, email }) + }) }) - ), - ]); + ) + ]) - return result; + return result } addMembers.schema = Joi.object() @@ -561,18 +561,18 @@ addMembers.schema = Joi.object() id: Joi.number().integer().required(), criteria: Joi.object() .keys({ - fields: Joi.string(), + fields: Joi.string() }) .required(), data: Joi.object() .keys({ handles: Joi.array().items(Joi.string()), - emails: Joi.array().items(Joi.string().email()), + emails: Joi.array().items(Joi.string().email()) }) .or('handles', 'emails') - .required(), + .required() }) - .required(); + .required() /** * Search members in a team. @@ -583,9 +583,9 @@ addMembers.schema = Joi.object() * @params {Object} criteria the search criteria * @returns {Object} the search result */ -async function searchMembers(currentUser, id, criteria) { - const result = await helper.listProjectMembers(currentUser, id, criteria); - return { result }; +async function searchMembers (currentUser, id, criteria) { + const result = await helper.listProjectMembers(currentUser, id, criteria) + return { result } } searchMembers.schema = Joi.object() @@ -595,11 +595,11 @@ searchMembers.schema = Joi.object() criteria: Joi.object() .keys({ role: Joi.string(), - fields: Joi.string(), + fields: Joi.string() }) - .required(), + .required() }) - .required(); + .required() /** * Search member invites for a team. @@ -610,13 +610,13 @@ searchMembers.schema = Joi.object() * @params {Object} criteria the search criteria * @returns {Object} the search result */ -async function searchInvites(currentUser, id, criteria) { +async function searchInvites (currentUser, id, criteria) { const result = await helper.listProjectMemberInvites( currentUser, id, criteria - ); - return { result }; + ) + return { result } } searchInvites.schema = Joi.object() @@ -625,11 +625,11 @@ searchInvites.schema = Joi.object() id: Joi.number().integer().required(), criteria: Joi.object() .keys({ - fields: Joi.string(), + fields: Joi.string() }) - .required(), + .required() }) - .required(); + .required() /** * Remove a member from a team. @@ -640,17 +640,17 @@ searchInvites.schema = Joi.object() * @param {String} projectMemberId the id of the project member * @returns {undefined} */ -async function deleteMember(currentUser, id, projectMemberId) { - await helper.deleteProjectMember(currentUser, id, projectMemberId); +async function deleteMember (currentUser, id, projectMemberId) { + await helper.deleteProjectMember(currentUser, id, projectMemberId) } deleteMember.schema = Joi.object() .keys({ currentUser: Joi.object().required(), id: Joi.number().integer().required(), - projectMemberId: Joi.number().integer().required(), + projectMemberId: Joi.number().integer().required() }) - .required(); + .required() /** * Return details about the current user. @@ -659,31 +659,31 @@ deleteMember.schema = Joi.object() * @params {Object} criteria the search criteria * @returns {Object} the user data for current user */ -async function getMe(currentUser) { - return helper.getUserByExternalId(currentUser.userId); +async function getMe (currentUser) { + return helper.getUserByExternalId(currentUser.userId) } getMe.schema = Joi.object() .keys({ - currentUser: Joi.object().required(), + currentUser: Joi.object().required() }) - .required(); + .required() /** * @param {Object} currentUser the user performing the operation. * @param {Object} data project data * @returns {Object} the created project */ -async function createProj(currentUser, data) { - return helper.createProject(currentUser, data); +async function createProj (currentUser, data) { + return helper.createProject(currentUser, data) } createProj.schema = Joi.object() .keys({ currentUser: Joi.object().required(), - data: Joi.object().required(), + data: Joi.object().required() }) - .required(); + .required() module.exports = { searchTeams, @@ -695,5 +695,5 @@ module.exports = { searchInvites, deleteMember, getMe, - createProj, -}; + createProj +} From 0304e683e2df5037ec0695c9faaf4b6d7fff5aa4 Mon Sep 17 00:00:00 2001 From: Cagdas U Date: Sat, 29 May 2021 13:04:56 +0300 Subject: [PATCH 09/23] feat(job-service): accept `roles` array * Add `roles` in Job schema. * Accept `roles` in the Job related endpoints. * Fix demo-data (was blocking `npm run local:init` script). --- data/demo-data.json | 12 ++++++------ src/common/helper.js | 1 + src/models/Job.js | 3 +++ src/services/JobService.js | 17 +++++++++++++---- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/data/demo-data.json b/data/demo-data.json index e0733443..8c5e8509 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -184,7 +184,7 @@ "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", "googleCalendarId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, "attendeesList": null, @@ -213,7 +213,7 @@ "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", "googleCalendarId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, "attendeesList": null, @@ -228,7 +228,7 @@ "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", "googleCalendarId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, "attendeesList": null, @@ -257,7 +257,7 @@ "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", "googleCalendarId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 3, "startTimestamp": null, "attendeesList": [ @@ -275,7 +275,7 @@ "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", "googleCalendarId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, "attendeesList": [ @@ -293,7 +293,7 @@ "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", "googleCalendarId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, "attendeesList": null, diff --git a/src/common/helper.js b/src/common/helper.js index e68e5c58..6333457e 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -111,6 +111,7 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { rateType: { type: 'keyword' }, workload: { type: 'keyword' }, skills: { type: 'keyword' }, + roles: { type: 'keyword' }, status: { type: 'keyword' }, isApplicationPageActive: { type: 'boolean' }, createdAt: { type: 'date' }, diff --git a/src/models/Job.js b/src/models/Job.js index 49d34ff7..62305000 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -94,6 +94,9 @@ module.exports = (sequelize) => { type: Sequelize.JSONB, allowNull: false }, + roles: { + type: Sequelize.ARRAY(Sequelize.UUID) + }, status: { type: Sequelize.STRING(255), allowNull: false diff --git a/src/services/JobService.js b/src/services/JobService.js index 61685901..482d0c34 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -177,6 +177,7 @@ createJob.schema = Joi.object().keys({ rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()).required(), + roles: Joi.array().items(Joi.string().uuid()).allow(null), isApplicationPageActive: Joi.boolean() }).required() }).required() @@ -245,6 +246,7 @@ partiallyUpdateJob.schema = Joi.object().keys({ rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()), + roles: Joi.array().items(Joi.string().uuid()).allow(null), isApplicationPageActive: Joi.boolean() }).required() }).required() @@ -361,6 +363,7 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } 'startDate', 'resourceType', 'skill', + 'role', 'rateType', 'workload', 'title', @@ -375,10 +378,10 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } } } } - } else if (key === 'skill') { + } else if (key === 'skill' || key === 'role') { must = { terms: { - skills: [value] + [`${key}s`]: [value] } } } else { @@ -453,9 +456,14 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } [Op.like]: `%${criteria.title}%` } } - if (criteria.skills) { + if (criteria.skill) { filter.skills = { - [Op.contains]: [criteria.skills] + [Op.contains]: [criteria.skill] + } + } + if (criteria.role) { + filter.roles = { + [Op.contains]: [criteria.role] } } if (criteria.jobIds && criteria.jobIds.length > 0) { @@ -495,6 +503,7 @@ searchJobs.schema = Joi.object().keys({ startDate: Joi.date(), resourceType: Joi.string(), skill: Joi.string().uuid(), + role: Joi.string().uuid(), rateType: Joi.rateType(), workload: Joi.workload(), status: Joi.jobStatus(), From 9732e1e179c33ee8852cd59ed14f95672afcb9c4 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Sun, 30 May 2021 23:23:27 +0300 Subject: [PATCH 10/23] fix: resource booking search issues --- data/demo-data.json | 5635 ++++++++++++++++- ...coder-bookings-api.postman_collection.json | 2056 +++++- ...topcoder-bookings.postman_environment.json | 221 +- src/common/helper.js | 1100 ++-- src/services/ResourceBookingService.js | 59 +- 5 files changed, 8040 insertions(+), 1031 deletions(-) diff --git a/data/demo-data.json b/data/demo-data.json index e0733443..57fcb97a 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -99,6 +99,497 @@ "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:12:09.293Z", "updatedAt": "2021-05-09T21:14:59.157Z" + }, + { + "id": "ff76b81d-f49b-4019-b50e-c7932a818f19", + "projectId": 17232, + "externalId": "51974177", + "description": "Welcome to the “Design Practice Challenge - Design a User Profile Screen”!\nThis challenge is created specifically for you, our new member, to learn/practice and get started with Design Competitions at Topcoder by participating and submitting to this very simple challenge where we are asking you to create a single design screen that shows a user’s profile information. You are free to create the design using design tools like Adobe XD, Figma, Sketch, or Photoshop!\nMake sure you register for the challenge and then read more details below and let us know if you have any doubts or questions in The Challenge Forum! Topcoder Design competitions provide coverage for a full range of design needs, from responsive and mobile application user experience to marketing collateral creation and support. To be specific, there are seven types of design challenges that we offer in the Design track: Application Front End Design Web Design Widget or Mobile Screen Design Wireframes Print/Presentation Design First2Finish Idea Generation\nYou must ensure your design submission addresses all the requirements mentioned in the challenge specification, and ensure your design follows the best practice for the specific interface or devices.\nTopcoder design challenges usually offer cash prizes for the three top winners and five checkpoint winners, and sometimes beyond that for special challenges like RUX (five top winners and eight rolling checkpoint winners) or LUX (eight top winners and eight rolling checkpoint winners).\nIn this practice challenge, we bring a basic Design Challenge for you so that you can adapt and get accustomed to the challenge phases.\nEach design challenge goes through the following time period phases: Registration - Duration in which you can register for the challenge 1st Round/Checkpoint Submission - Duration in which you can submit according to round one requirements Checkpoint Screening - Duration in which a Topcoder Screener will check your checkpoint design submission Checkpoint Review - Duration in which DRB and client will review submissions from round one and provide feedback 2nd Round/Final Submission - Duration in which all first-round submitters can submit design work after applying the checkpoint feedback received from DRB and Client\nAfter your final submission, the following phases take place: Final Screening, Final Review, Final Fixes, and Approval.\nThe majority of design challenges at Topcoder usually use the two-round format unless it is a one-round type challenge such as a Design First2Finish, Rapid User Experience (RUX), or Live User Experience (LUX)\nFor more help, we have a Detailed Guide about how to compete in a Design Challenge or check out This Video.\nMake sure you register for the challenge and then read more details below and let us know if you have any doubts or questions in The Challenge Forum!\nOVERVIEW Create a single UI Design Screen for any web device for a user’s profile.\nCHALLENGE REQUIREMENTS Please make sure to create and include the following requests in the User Profile screen: Name and Profile Picture such that the audience can easily recognize the user. About to give some brief information about the user. Skills and Specialization to emphasize a user’s strength. Profile links linking to user’s other important and relevant websites (Facebook, Twitter, Dribbble, Github, etc) You can check a sample of Topcoder existing user profiles to get an idea Here Feel free to use and copy content from Topcoder’s Profile Page and create your own version for the user profile screen Quotes? Hobbies? Interest? Etc? Feel free to add as many details as you want to make your design better!\nTARGET AUDIENCE Topcoder members that want to know about you and your history at Topcoder Potential clients that want to know about your skills and achievement at Topcoder before they hire you\nJUDGING CRITERIA SCORE Creativity: 8 1: barely new ideas Aesthetics: 10 1: low-fidelity design, wireframe or plain sketch Exploration: 7 1: strictly follow an existing reference or production guideline Branding: 10 1: don’t care at all about the branding just functionality\nBRANDING GUIDELINES Open to Designers (Font, Colors, Style, etc)\nTARGET DEVICES Web App: Minimum 1440px width with height adjusted accordingly\nFINAL SUBMISSION GUIDELINES All original source files created in Adobe XD, Adobe Photoshop, Figma, or Sketch Marvel Prototype: Upload your screens to Marvel App Ask for Marvel Prototype access in the challenge forum Include your Marvel App URL as a text file in your final submission. Label the file as “MarvelApp URL”", + "title": "Job Taas", + "startDate": "2021-03-08T15:00:00.000Z", + "duration": 4, + "numPositions": 2, + "resourceType": "software-developer", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "4cb4c2c3-f8af-49e4-8b67-6318d5d1d329", + "ad43a3f3-413f-4bfe-9703-28afad49b116", + "d67a5932-3f8d-4a5a-88cf-d7b706aae2d5", + "b4e1d1d2-794f-486d-8629-b9d7f26af5c6", + "0b104b7c-0792-4118-8bc7-a274e9ee19e3" + ], + "status": "closed", + "isApplicationPageActive": false, + "createdBy": "fe38eed1-af73-41fd-85a2-ac4da1ff09a3", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-03-19T08:11:26.966Z", + "updatedAt": "2021-03-23T04:35:03.686Z" + }, + { + "id": "ff753824-919c-4712-9197-49d7edaa4db7", + "projectId": 17430, + "externalId": "54439701", + "description": null, + "title": "job-Thu May 27 2021 11:58:55 GMT+0530 (India Standard Time)", + "startDate": null, + "duration": null, + "numPositions": 1, + "resourceType": null, + "rateType": null, + "workload": null, + "skills": [ + "5843b329-433c-4a17-a5b2-570caaa34baf" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "71c5e6a8-51d9-4fb5-91ce-d974642531af", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-27T06:28:58.039Z", + "updatedAt": "2021-05-27T06:29:06.204Z" + }, + { + "id": "ff3feeae-d4f7-457c-bff7-215be5efe2b8", + "projectId": 16781, + "externalId": "0", + "description": "Designer", + "title": "Designer", + "startDate": "2020-12-14T12:41:13.019Z", + "duration": null, + "numPositions": 5, + "resourceType": "desiger", + "rateType": "hourly", + "workload": "full-time", + "skills": [], + "status": "assigned", + "isApplicationPageActive": false, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2020-12-14T12:41:14.323Z", + "updatedAt": "2021-02-01T07:14:33.994Z" + }, + { + "id": "fefd2618-9b66-4431-9874-1d02d7a37d90", + "projectId": 17324, + "externalId": "53432162", + "description": "

Python Data Science

", + "title": "Data Scientist", + "startDate": "2021-07-06T18:30:00.000Z", + "duration": 9, + "numPositions": 2, + "resourceType": "data-scientist", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "2752d41b-1082-478b-9fd4-6396f510a130", + "bfc796ee-0c25-4838-ab23-5b007dee7672" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-13T08:51:26.517Z", + "updatedAt": "2021-05-13T08:52:02.354Z" + }, + { + "id": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "projectId": 17103, + "externalId": null, + "description": "# Heading\n\n**bold**\n*italic*\n~~crossed~~\n\n- - -\n\n> test\n\n* 1\n * 2\n\n1. 2\n 1. 3\n\n* [ ] asdfsdf\n\n
\n| 1 | 1 |\n| --- | --- |\n| 2 | 2 |\n\n![test](https://user-images.githubusercontent.com/146016/108334984-235ce880-71db-11eb-8643-263675197b2c.png)\n[https://user-images.githubusercontent.com/146016/108334984-235ce880-71db-11eb-8643-263675197b2c.png](https://user-images.githubusercontent.com/146016/108334984-235ce880-71db-11eb-8643-263675197b2c.png)\n`asdfsadfasdf`\n\n
\n```\nasdsdf\n```", + "title": "1", + "startDate": null, + "duration": 1, + "numPositions": 10, + "resourceType": "software-developer", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "24ba39a5-0b75-41f3-9a33-4f8063fcd828" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "077fd578-7463-457f-b6ef-22c02178d7f5", + "updatedBy": null, + "createdAt": "2021-02-21T09:26:04.620Z", + "updatedAt": "2021-02-21T09:26:04.620Z" + }, + { + "id": "fed687e1-4257-48bb-806c-38712f9bf14f", + "projectId": 16870, + "externalId": "1212", + "description": "Dummy Description", + "title": "Dummy title - at most 64 characters", + "startDate": "2020-09-27T04:17:23.131Z", + "duration": 2, + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "monthly", + "workload": "full-time", + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "077fd578-7463-457f-b6ef-22c02178d7f5", + "createdAt": "2021-02-04T10:25:20.358Z", + "updatedAt": "2021-02-18T13:48:13.326Z" + }, + { + "id": "fed14737-9ea2-4d90-b26c-781ad689b4ae", + "projectId": 16949, + "externalId": null, + "description": null, + "title": "Test", + "startDate": null, + "duration": null, + "numPositions": 1, + "resourceType": null, + "rateType": "weekly", + "workload": "full-time", + "skills": [], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", + "updatedBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", + "createdAt": "2021-01-31T20:29:34.581Z", + "updatedAt": "2021-01-31T22:00:51.803Z" + }, + { + "id": "fe8da845-5313-496f-b859-9824bd06a0db", + "projectId": 16870, + "externalId": "1212", + "description": null, + "title": "Max Dummy title - at most 64 characters", + "startDate": null, + "duration": 3, + "numPositions": 13, + "resourceType": null, + "rateType": null, + "workload": null, + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "in-review", + "isApplicationPageActive": false, + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedBy": "077fd578-7463-457f-b6ef-22c02178d7f5", + "createdAt": "2021-01-11T08:29:29.153Z", + "updatedAt": "2021-02-18T13:48:30.718Z" + }, + { + "id": "fe600350-0a6d-4dac-922f-a6a7d285daa1", + "projectId": 17290, + "externalId": "52700649", + "description": "test", + "title": "PICACHUI APR 30 JOB 5", + "startDate": "2021-04-30T18:30:00.000Z", + "duration": 5, + "numPositions": 5, + "resourceType": "software-developer", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "ad43a3f3-413f-4bfe-9703-28afad49b116" + ], + "status": "in-review", + "isApplicationPageActive": false, + "createdBy": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-04-30T07:56:22.940Z", + "updatedAt": "2021-04-30T08:06:25.212Z" + }, + { + "id": "fe539bef-9119-4a8c-b7b0-915e7e3a3ba3", + "projectId": 16870, + "externalId": "1212", + "description": "Dummy Description", + "title": "Dummy title - at most 64 characters", + "startDate": "2020-09-27T04:17:23.131Z", + "duration": null, + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedBy": null, + "createdAt": "2021-01-07T19:42:10.255Z", + "updatedAt": "2021-05-30T11:14:08.434Z" + }, + { + "id": "fe481d1c-cf87-49c1-9370-695f9f754041", + "projectId": 16762, + "externalId": "0", + "description": "Designer #1", + "title": "Designer #1", + "startDate": "2020-09-27T04:17:23.131Z", + "duration": null, + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "ee4c50c1-c8c3-475e-b6b6-edbd136a19d6", + "89139c80-d0a2-47c2-aa16-14589d5afd10", + "9f2d9127-6a2e-4506-ad76-c4ab63577b09", + "9515e7ee-83b6-49d1-ba5c-6c59c5a8ef1b", + "c854ab55-5922-4be1-8ecc-b3bc1f8629af", + "8456002e-fa2d-44f0-b0e7-86b1c02b6e4c", + "114b4ec8-805e-4c60-b351-14a955a991a9", + "213408aa-f16f-46c8-bc57-9e569cee3f11", + "b37a48db-f775-4e4e-b403-8ad1d234cdea", + "99b930b5-1b91-4df1-8b17-d9307107bb51", + "6388a632-c3ad-4525-9a73-66a527c03672", + "23839f38-6f19-4de9-9d28-f020056bca73", + "289e42a3-23e9-49be-88e1-6deb93cd8c31", + "b403f209-63b5-42bc-9b5f-1564416640d8" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedBy": null, + "createdAt": "2020-12-08T15:56:18.707Z", + "updatedAt": "2021-05-30T11:14:08.434Z" + }, + { + "id": "fe270791-bc24-4f6a-8c1b-b897f5d97d2f", + "projectId": 16899, + "externalId": null, + "description": "Software Developer Full time", + "title": "Software Developer", + "startDate": null, + "duration": null, + "numPositions": 1, + "resourceType": "software-developer", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "e2b8acc2-881f-45a6-8321-489976b1db21", + "4fce6ced-3610-443c-92eb-3f6d76b34f5c", + "5c6c79ee-a8fe-4ec2-b9df-813bd67df413", + "24ba39a5-0b75-41f3-9a33-4f8063fcd828", + "4cb4c2c3-f8af-49e4-8b67-6318d5d1d329", + "1fd02aad-e08a-4669-9ffd-181468fea694" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": null, + "createdAt": "2021-01-12T08:57:01.987Z", + "updatedAt": "2021-05-30T11:14:08.434Z" + }, + { + "id": "fd48d96e-b0f2-43b7-8a48-f4fa194d6bc8", + "projectId": 17363, + "externalId": "54085472", + "description": "Test", + "title": "Test Job May 25", + "startDate": null, + "duration": 2, + "numPositions": 5, + "resourceType": "designer", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "24ba39a5-0b75-41f3-9a33-4f8063fcd828" + ], + "status": "in-review", + "isApplicationPageActive": false, + "createdBy": "fe38eed1-af73-41fd-85a2-ac4da1ff09a3", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-25T11:03:00.164Z", + "updatedAt": "2021-05-27T04:48:06.194Z" + }, + { + "id": "fd13ad99-f16a-4362-9274-80f5f38895c3", + "projectId": 17300, + "externalId": "52885626", + "description": "Description", + "title": "Test Job RB 3", + "startDate": null, + "duration": 1, + "numPositions": 3, + "resourceType": "designer", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "4fce6ced-3610-443c-92eb-3f6d76b34f5c" + ], + "status": "in-review", + "isApplicationPageActive": false, + "createdBy": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-01T09:43:13.357Z", + "updatedAt": "2021-05-01T09:44:53.411Z" + }, + { + "id": "fc5ba131-566f-46fe-8501-79c593241896", + "projectId": 16739, + "externalId": "0", + "description": "Designer", + "title": "Designer", + "startDate": "2020-12-07T09:30:37.304Z", + "duration": null, + "numPositions": 12, + "resourceType": "desiger", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "ee4c50c1-c8c3-475e-b6b6-edbd136a19d6", + "89139c80-d0a2-47c2-aa16-14589d5afd10", + "c854ab55-5922-4be1-8ecc-b3bc1f8629af", + "38471151-8513-4e04-b0fe-80331556abd9", + "8456002e-fa2d-44f0-b0e7-86b1c02b6e4c", + "9515e7ee-83b6-49d1-ba5c-6c59c5a8ef1b", + "b37a48db-f775-4e4e-b403-8ad1d234cdea", + "9f2d9127-6a2e-4506-ad76-c4ab63577b09", + "213408aa-f16f-46c8-bc57-9e569cee3f11", + "114b4ec8-805e-4c60-b351-14a955a991a9", + "afc39636-f7d7-4349-aef5-f9a76cc9e155", + "c2ac5154-6a0d-425d-9c90-9fd0af5d88a5", + "bfc796ee-0c25-4838-ab23-5b007dee7672", + "fbf792fc-0920-4b54-866a-f48c386994a0", + "63189fe0-94a9-4e2e-90f3-ef764e6a005b", + "87a8dd0f-12ea-47cd-a80d-20f47f0b7b9a", + "7a359e66-82bd-429c-8f95-8913bf00e67b" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedBy": null, + "createdAt": "2020-12-07T09:30:37.470Z", + "updatedAt": "2021-05-30T11:14:08.434Z" + }, + { + "id": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "projectId": 16805, + "externalId": "1212", + "description": "QA Demo Description", + "title": "QA Demo Description", + "startDate": "2020-09-27T04:17:23.131Z", + "duration": null, + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "cancelled", + "isApplicationPageActive": false, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2020-12-24T06:48:56.943Z", + "updatedAt": "2020-12-24T06:49:28.997Z" + }, + { + "id": "fc2b006d-997b-49c3-a414-59ee54a48f9f", + "projectId": 16706, + "externalId": "10003", + "description": "Dummy10003 Description", + "title": "Dummy10003 Description", + "startDate": "2020-11-28T04:17:23.131Z", + "duration": null, + "numPositions": 9, + "resourceType": "Dummy Resource Type", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "cb01fd31-e8d2-4e34-8bf3-b149705de3e1" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedBy": null, + "createdAt": "2020-11-20T05:30:03.014Z", + "updatedAt": "2021-05-30T11:14:08.434Z" + }, + { + "id": "fc0240f0-8c8f-40ce-a551-e83b45673098", + "projectId": 16714, + "externalId": "2227", + "description": "zapier2227 Description", + "title": "zapier2227 Description", + "startDate": "2021-01-04T04:17:23.131Z", + "duration": null, + "numPositions": 8, + "resourceType": "Dummy2227 Resource Type", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedBy": null, + "createdAt": "2020-12-08T13:50:26.805Z", + "updatedAt": "2021-05-30T11:14:08.434Z" + }, + { + "id": "fb8b92f6-4ffb-4ba6-8c38-c2d4a151f76b", + "projectId": 17091, + "externalId": "12312", + "description": "second Job created from api", + "title": "second Job from api", + "startDate": "2021-02-27T04:17:23.131Z", + "duration": null, + "numPositions": 12, + "resourceType": "software Developer", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "71c5e6a8-51d9-4fb5-91ce-d974642531af", + "updatedBy": null, + "createdAt": "2021-02-18T09:43:11.985Z", + "updatedAt": "2021-02-18T09:43:11.985Z" + }, + { + "id": "fb2f5f9b-5874-4dcd-af94-727fc0409760", + "projectId": 16718, + "externalId": "12131", + "description": "# Awesome Editor!\n\nIt has been *released as opensource in 2018* and has\n~~continually~~\n evolved to **receive 10k GitHub ⭐️ Stars**.\n\n## Create Instance\n\nYou can create an instance with the following code and use `getHtml()` and `getMarkdown()` of the [Editor](https://github.com/nhn/tui.editor).\n
\n``` js\nconst editor = new Editor(options);\n```\n\n> See the table below for default options\n> \n> \n> \n> > More API information can be found in the document\n\n| name | type | description |\n| ---- | ---- | ----------- |\n| el | `HTMLElement` | container element |\n\n## Features\n\n* CommonMark + GFM Specifications\n * Live Preview\n * Scroll Sync\n * Auto Indent\n * Syntax Highlight\n 1. Markdown\n 2. Preview\n\n## Support Wrappers\n\n> * Wrappers\n> \n> 1. [x] React\n> 2. [x] Vue\n> 3. [ ] Ember", + "title": "aaaaa", + "startDate": "2021-02-27T04:17:23.131Z", + "duration": null, + "numPositions": 3, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "workload": "full-time", + "skills": [], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "077fd578-7463-457f-b6ef-22c02178d7f5", + "createdAt": "2021-01-25T10:07:56.847Z", + "updatedAt": "2021-02-23T13:02:59.708Z" } ], "JobCandidate": [ @@ -181,18 +672,29 @@ "interviews": [ { "id": "077aa2ca-5b60-4ad9-a965-1b37e08a5046", + "xaiId": null, "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", - "googleCalendarId": null, - "customMessage": null, - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 1, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Completed", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:16:10.887Z", - "updatedAt": "2021-05-09T21:16:10.887Z" + "updatedAt": "2021-05-09T21:16:10.887Z", + "deletedAt": null } ] }, @@ -210,33 +712,55 @@ "interviews": [ { "id": "b1f7ba76-640f-47e2-9463-59e51b51ec60", + "xaiId": null, "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 2, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Scheduling", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:17:23.517Z", - "updatedAt": "2021-05-09T21:17:23.517Z" + "updatedAt": "2021-05-09T21:17:23.517Z", + "deletedAt": null }, { "id": "3144fa65-ea1a-4bec-81b0-7cb1c8845826", + "xaiId": null, "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": null, - "customMessage": null, - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 1, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Completed", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:16:39.019Z", - "updatedAt": "2021-05-09T21:16:39.019Z" + "updatedAt": "2021-05-09T21:16:39.019Z", + "deletedAt": null } ] }, @@ -254,54 +778,81 @@ "interviews": [ { "id": "976d23a9-5710-453f-99d9-f57a588bb610", + "xaiId": null, "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 3, "startTimestamp": null, - "attendeesList": [ - "attendee1@yopmail.com", - "attendee2@yopmail.com" - ], + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Scheduling", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:21:28.713Z", - "updatedAt": "2021-05-09T21:21:28.713Z" + "updatedAt": "2021-05-09T21:21:28.713Z", + "deletedAt": null }, { "id": "a23e1bf2-1084-4cfe-a0d8-d83bc6fec655", + "xaiId": null, "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 2, "startTimestamp": null, - "attendeesList": [ - "attendee1@yopmail.com", - "attendee2@yopmail.com" - ], + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Scheduling", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:21:22.428Z", - "updatedAt": "2021-05-09T21:21:22.428Z" + "updatedAt": "2021-05-09T21:21:22.428Z", + "deletedAt": null }, { "id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb", + "xaiId": null, "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": null, - "customMessage": null, - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 1, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Completed", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:21:17.346Z", - "updatedAt": "2021-05-09T21:21:17.346Z" + "updatedAt": "2021-05-09T21:21:17.346Z", + "deletedAt": null } ] }, @@ -640,16 +1191,16 @@ "updatedAt": "2021-05-09T21:45:32.659Z", "payments": [ { - "id": "03a0163c-472e-4ea6-b8ad-3dc86d418ecf", + "id": "1c682ea9-ba63-4fcc-b00c-049d2458d3ac", "workPeriodId": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 210.19, - "status": "cancelled", + "amount": 57.79, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:36.932Z", - "updatedAt": "2021-05-09T21:31:36.932Z" + "createdAt": "2021-05-09T21:31:35.726Z", + "updatedAt": "2021-05-09T21:31:35.726Z" }, { "id": "14b266c6-e76a-4042-b439-74fe3e42c90f", @@ -664,16 +1215,16 @@ "updatedAt": "2021-05-09T21:31:38.183Z" }, { - "id": "1c682ea9-ba63-4fcc-b00c-049d2458d3ac", + "id": "03a0163c-472e-4ea6-b8ad-3dc86d418ecf", "workPeriodId": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 57.79, - "status": "completed", + "amount": 210.19, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:35.726Z", - "updatedAt": "2021-05-09T21:31:35.726Z" + "createdAt": "2021-05-09T21:31:36.932Z", + "updatedAt": "2021-05-09T21:31:36.932Z" } ] }, @@ -694,28 +1245,28 @@ "updatedAt": "2021-05-09T21:45:37.647Z", "payments": [ { - "id": "e8f3d379-f5a0-47f6-b37b-cae24f5909e9", + "id": "cc235aee-0911-4869-bb49-911507bb31e7", "workPeriodId": "e8346d7b-4ada-428d-a768-c2989306f63a", "challengeId": "00000000-0000-0000-0000-000000000000", "amount": 494.46, - "status": "completed", + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:22.403Z", - "updatedAt": "2021-05-09T21:34:22.403Z" + "createdAt": "2021-05-09T21:34:26.807Z", + "updatedAt": "2021-05-09T21:34:26.807Z" }, { - "id": "cc235aee-0911-4869-bb49-911507bb31e7", + "id": "e8f3d379-f5a0-47f6-b37b-cae24f5909e9", "workPeriodId": "e8346d7b-4ada-428d-a768-c2989306f63a", "challengeId": "00000000-0000-0000-0000-000000000000", "amount": 494.46, - "status": "cancelled", + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:26.807Z", - "updatedAt": "2021-05-09T21:34:26.807Z" + "createdAt": "2021-05-09T21:34:22.403Z", + "updatedAt": "2021-05-09T21:34:22.403Z" } ] }, @@ -796,28 +1347,28 @@ "updatedAt": "2021-05-09T21:45:27.504Z", "payments": [ { - "id": "fcd10a26-3548-4f9b-9e2b-20397d057800", + "id": "40e862a0-8772-4587-88b4-23acff8eb2e0", "workPeriodId": "61c1e7e3-5e0a-4892-9099-872bc4c11a22", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 494.46, + "amount": 417.42, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:41.381Z", - "updatedAt": "2021-05-09T21:32:41.381Z" + "createdAt": "2021-05-09T21:32:40.091Z", + "updatedAt": "2021-05-09T21:32:40.091Z" }, { - "id": "40e862a0-8772-4587-88b4-23acff8eb2e0", + "id": "fcd10a26-3548-4f9b-9e2b-20397d057800", "workPeriodId": "61c1e7e3-5e0a-4892-9099-872bc4c11a22", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 417.42, + "amount": 494.46, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:40.091Z", - "updatedAt": "2021-05-09T21:32:40.091Z" + "createdAt": "2021-05-09T21:32:41.381Z", + "updatedAt": "2021-05-09T21:32:41.381Z" } ] } @@ -857,28 +1408,28 @@ "updatedAt": "2021-05-09T21:46:59.354Z", "payments": [ { - "id": "c8be508d-2eb5-4712-8bd7-1b28e870abc2", + "id": "9785ae89-05dc-4bcc-a030-52bd0e681d41", "workPeriodId": "b0758857-0221-47a5-a444-e263e5d9e1cf", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, - "status": "completed", + "amount": 168.54, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:02.733Z", - "updatedAt": "2021-05-09T21:34:02.733Z" + "createdAt": "2021-05-09T21:34:01.410Z", + "updatedAt": "2021-05-09T21:34:01.410Z" }, { - "id": "9785ae89-05dc-4bcc-a030-52bd0e681d41", + "id": "c8be508d-2eb5-4712-8bd7-1b28e870abc2", "workPeriodId": "b0758857-0221-47a5-a444-e263e5d9e1cf", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 168.54, - "status": "cancelled", + "amount": 448.51, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:01.410Z", - "updatedAt": "2021-05-09T21:34:01.410Z" + "createdAt": "2021-05-09T21:34:02.733Z", + "updatedAt": "2021-05-09T21:34:02.733Z" } ] }, @@ -899,28 +1450,28 @@ "updatedAt": "2021-05-09T21:47:05.373Z", "payments": [ { - "id": "c3b71f96-7680-459b-82b2-f6eb3c3f6c8f", + "id": "3ed31706-0e99-4084-81f4-b126a1a68db6", "workPeriodId": "176db0d0-474f-4590-831a-547d596c01b4", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 119.32, + "amount": 144.16, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:18.342Z", - "updatedAt": "2021-05-09T21:31:18.342Z" + "createdAt": "2021-05-09T21:31:17.193Z", + "updatedAt": "2021-05-09T21:31:17.193Z" }, { - "id": "3ed31706-0e99-4084-81f4-b126a1a68db6", + "id": "c3b71f96-7680-459b-82b2-f6eb3c3f6c8f", "workPeriodId": "176db0d0-474f-4590-831a-547d596c01b4", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 144.16, + "amount": 119.32, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:17.193Z", - "updatedAt": "2021-05-09T21:31:17.193Z" + "createdAt": "2021-05-09T21:31:18.342Z", + "updatedAt": "2021-05-09T21:31:18.342Z" } ] }, @@ -941,28 +1492,28 @@ "updatedAt": "2021-05-09T21:47:12.015Z", "payments": [ { - "id": "69445cbf-6d94-49a5-b2aa-65459ec78594", + "id": "663f11df-7832-431a-a46e-ad8c890ae52b", "workPeriodId": "5a174833-cb08-49f5-b077-cffb8e60ca01", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 417.42, - "status": "completed", + "amount": 55.6, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:33.664Z", - "updatedAt": "2021-05-09T21:32:33.664Z" + "createdAt": "2021-05-09T21:32:32.606Z", + "updatedAt": "2021-05-09T21:32:32.606Z" }, { - "id": "663f11df-7832-431a-a46e-ad8c890ae52b", + "id": "69445cbf-6d94-49a5-b2aa-65459ec78594", "workPeriodId": "5a174833-cb08-49f5-b077-cffb8e60ca01", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 55.6, - "status": "cancelled", + "amount": 417.42, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:32.606Z", - "updatedAt": "2021-05-09T21:32:32.606Z" + "createdAt": "2021-05-09T21:32:33.664Z", + "updatedAt": "2021-05-09T21:32:33.664Z" } ] }, @@ -983,28 +1534,28 @@ "updatedAt": "2021-05-09T21:47:25.687Z", "payments": [ { - "id": "8eb8fc37-5ab0-4480-8806-3d3c57ab38e1", + "id": "f65930b7-d61d-4923-bdab-54848661f151", "workPeriodId": "8c9db4fd-78ad-4e59-acba-462487b74c3a", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 496.54, + "amount": 57.79, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:07.419Z", - "updatedAt": "2021-05-09T21:33:07.419Z" + "createdAt": "2021-05-09T21:33:06.108Z", + "updatedAt": "2021-05-09T21:33:06.108Z" }, { - "id": "f65930b7-d61d-4923-bdab-54848661f151", + "id": "8eb8fc37-5ab0-4480-8806-3d3c57ab38e1", "workPeriodId": "8c9db4fd-78ad-4e59-acba-462487b74c3a", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 57.79, + "amount": 496.54, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:06.108Z", - "updatedAt": "2021-05-09T21:33:06.108Z" + "createdAt": "2021-05-09T21:33:07.419Z", + "updatedAt": "2021-05-09T21:33:07.419Z" } ] }, @@ -1025,28 +1576,28 @@ "updatedAt": "2021-05-09T21:47:30.586Z", "payments": [ { - "id": "c456755e-0432-4656-848b-64f9c5dc8f25", + "id": "dd8f5c08-d6a1-4fd2-b6bd-85fb6425d13d", "workPeriodId": "18881107-cc17-4087-9b2b-a74f04187f73", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 32.92, - "status": "cancelled", + "amount": 372.18, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:27.102Z", - "updatedAt": "2021-05-09T21:31:27.102Z" + "createdAt": "2021-05-09T21:31:25.690Z", + "updatedAt": "2021-05-09T21:31:25.690Z" }, { - "id": "dd8f5c08-d6a1-4fd2-b6bd-85fb6425d13d", + "id": "c456755e-0432-4656-848b-64f9c5dc8f25", "workPeriodId": "18881107-cc17-4087-9b2b-a74f04187f73", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 372.18, - "status": "completed", + "amount": 32.92, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:25.690Z", - "updatedAt": "2021-05-09T21:31:25.690Z" + "createdAt": "2021-05-09T21:31:27.102Z", + "updatedAt": "2021-05-09T21:31:27.102Z" } ] }, @@ -1067,28 +1618,28 @@ "updatedAt": "2021-05-09T21:47:19.034Z", "payments": [ { - "id": "10584b23-5ab2-44e2-a927-e020c08e4f84", + "id": "6d59a499-44e3-41e2-8368-5baee86dd8ab", "workPeriodId": "9b455e21-e186-4622-923a-f115d23549d1", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 293.79, + "amount": 57.79, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:29.348Z", - "updatedAt": "2021-05-09T21:33:29.348Z" + "createdAt": "2021-05-09T21:33:28.108Z", + "updatedAt": "2021-05-09T21:33:28.108Z" }, { - "id": "6d59a499-44e3-41e2-8368-5baee86dd8ab", + "id": "10584b23-5ab2-44e2-a927-e020c08e4f84", "workPeriodId": "9b455e21-e186-4622-923a-f115d23549d1", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 57.79, + "amount": 293.79, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:28.108Z", - "updatedAt": "2021-05-09T21:33:28.108Z" + "createdAt": "2021-05-09T21:33:29.348Z", + "updatedAt": "2021-05-09T21:33:29.348Z" } ] } @@ -1128,28 +1679,28 @@ "updatedAt": "2021-05-09T21:46:12.086Z", "payments": [ { - "id": "00640d2d-8330-445a-b022-aa687033b2b3", + "id": "05d09e2b-02a0-4d33-b6db-0f69a98154c6", "workPeriodId": "ac0ae325-8d77-4a73-bd85-5361165801cd", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 275.73, + "amount": 374.34, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:54.315Z", - "updatedAt": "2021-05-09T21:33:54.315Z" + "createdAt": "2021-05-09T21:33:53.131Z", + "updatedAt": "2021-05-09T21:33:53.131Z" }, { - "id": "05d09e2b-02a0-4d33-b6db-0f69a98154c6", + "id": "00640d2d-8330-445a-b022-aa687033b2b3", "workPeriodId": "ac0ae325-8d77-4a73-bd85-5361165801cd", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 374.34, + "amount": 275.73, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:53.131Z", - "updatedAt": "2021-05-09T21:33:53.131Z" + "createdAt": "2021-05-09T21:33:54.315Z", + "updatedAt": "2021-05-09T21:33:54.315Z" } ] }, @@ -1230,28 +1781,28 @@ "updatedAt": "2021-05-09T21:46:02.038Z", "payments": [ { - "id": "be9706ac-c6cb-4fff-894b-8719bcf634dc", + "id": "c7013bf0-17b5-4b15-826b-385fad41caf4", "workPeriodId": "94dde794-b730-4e05-8ea6-dcc5b541d43e", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 275.73, + "amount": 144.16, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:21.933Z", - "updatedAt": "2021-05-09T21:33:21.933Z" + "createdAt": "2021-05-09T21:33:20.680Z", + "updatedAt": "2021-05-09T21:33:20.680Z" }, { - "id": "c7013bf0-17b5-4b15-826b-385fad41caf4", + "id": "be9706ac-c6cb-4fff-894b-8719bcf634dc", "workPeriodId": "94dde794-b730-4e05-8ea6-dcc5b541d43e", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 144.16, + "amount": 275.73, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:20.680Z", - "updatedAt": "2021-05-09T21:33:20.680Z" + "createdAt": "2021-05-09T21:33:21.933Z", + "updatedAt": "2021-05-09T21:33:21.933Z" } ] }, @@ -1272,52 +1823,52 @@ "updatedAt": "2021-05-09T21:45:53.796Z", "payments": [ { - "id": "fc577d14-78e8-404c-a17b-ab496e4041d8", + "id": "6e1a114f-bfac-4ab8-93d6-e47206200540", "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 203.74, + "amount": 494.46, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:41.520Z", - "updatedAt": "2021-05-09T21:34:41.520Z" + "createdAt": "2021-05-09T21:34:40.036Z", + "updatedAt": "2021-05-09T21:34:40.036Z" }, { - "id": "fbc2d96f-f6c6-4a4d-b737-14a3564b7f70", + "id": "10fd3b3e-f5b2-42cc-91d4-54c73c003aae", "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, - "status": "cancelled", + "amount": 477.97, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:38.700Z", - "updatedAt": "2021-05-09T21:34:38.700Z" + "createdAt": "2021-05-09T21:34:37.374Z", + "updatedAt": "2021-05-09T21:34:37.374Z" }, { - "id": "10fd3b3e-f5b2-42cc-91d4-54c73c003aae", + "id": "fbc2d96f-f6c6-4a4d-b737-14a3564b7f70", "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 477.97, - "status": "completed", + "amount": 448.51, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:37.374Z", - "updatedAt": "2021-05-09T21:34:37.374Z" + "createdAt": "2021-05-09T21:34:38.700Z", + "updatedAt": "2021-05-09T21:34:38.700Z" }, { - "id": "6e1a114f-bfac-4ab8-93d6-e47206200540", + "id": "fc577d14-78e8-404c-a17b-ab496e4041d8", "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 494.46, + "amount": 203.74, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:40.036Z", - "updatedAt": "2021-05-09T21:34:40.036Z" + "createdAt": "2021-05-09T21:34:41.520Z", + "updatedAt": "2021-05-09T21:34:41.520Z" } ] }, @@ -1387,28 +1938,28 @@ "updatedAt": "2021-05-09T21:48:08.381Z", "payments": [ { - "id": "fa9bd31c-6c83-4ee4-9d45-a833cfe821f5", + "id": "abb79afc-a370-4625-a067-a3b57c9b4700", "workPeriodId": "d111a56f-593d-452e-9787-551bea504c92", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 271.42, + "amount": 448.51, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:11.662Z", - "updatedAt": "2021-05-09T21:34:11.662Z" + "createdAt": "2021-05-09T21:34:10.371Z", + "updatedAt": "2021-05-09T21:34:10.371Z" }, { - "id": "abb79afc-a370-4625-a067-a3b57c9b4700", + "id": "fa9bd31c-6c83-4ee4-9d45-a833cfe821f5", "workPeriodId": "d111a56f-593d-452e-9787-551bea504c92", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, + "amount": 271.42, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:10.371Z", - "updatedAt": "2021-05-09T21:34:10.371Z" + "createdAt": "2021-05-09T21:34:11.662Z", + "updatedAt": "2021-05-09T21:34:11.662Z" } ] }, @@ -1429,28 +1980,28 @@ "updatedAt": "2021-05-09T21:47:38.022Z", "payments": [ { - "id": "c658d66e-86e1-49c7-8051-2b9a017935ad", + "id": "3770680c-8045-43d4-8baf-cb7b3b714d39", "workPeriodId": "061f31fb-4f8c-462f-92c2-e5d275c45fde", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 248.38, - "status": "completed", + "amount": 477.97, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:41.304Z", - "updatedAt": "2021-05-09T21:30:41.304Z" + "createdAt": "2021-05-09T21:30:42.711Z", + "updatedAt": "2021-05-09T21:30:42.711Z" }, { - "id": "3770680c-8045-43d4-8baf-cb7b3b714d39", + "id": "c658d66e-86e1-49c7-8051-2b9a017935ad", "workPeriodId": "061f31fb-4f8c-462f-92c2-e5d275c45fde", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 477.97, - "status": "cancelled", + "amount": 248.38, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:42.711Z", - "updatedAt": "2021-05-09T21:30:42.711Z" + "createdAt": "2021-05-09T21:30:41.304Z", + "updatedAt": "2021-05-09T21:30:41.304Z" } ] }, @@ -1471,28 +2022,28 @@ "updatedAt": "2021-05-09T21:47:44.291Z", "payments": [ { - "id": "be4d5099-8b8e-45e2-b6cd-ab1997f57e26", + "id": "dfc9bed6-78f2-407e-a7e4-abea9a3d3b46", "workPeriodId": "5904b1d9-cb50-4b5d-8103-6741fec2f86b", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 477.97, - "status": "completed", + "amount": 48.51, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:26.174Z", - "updatedAt": "2021-05-09T21:32:26.174Z" + "createdAt": "2021-05-09T21:32:27.398Z", + "updatedAt": "2021-05-09T21:32:27.398Z" }, { - "id": "dfc9bed6-78f2-407e-a7e4-abea9a3d3b46", + "id": "be4d5099-8b8e-45e2-b6cd-ab1997f57e26", "workPeriodId": "5904b1d9-cb50-4b5d-8103-6741fec2f86b", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 48.51, - "status": "cancelled", + "amount": 477.97, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:27.398Z", - "updatedAt": "2021-05-09T21:32:27.398Z" + "createdAt": "2021-05-09T21:32:26.174Z", + "updatedAt": "2021-05-09T21:32:26.174Z" } ] }, @@ -1513,28 +2064,28 @@ "updatedAt": "2021-05-09T21:47:58.216Z", "payments": [ { - "id": "3bf29da2-581c-4f9c-8b0f-ff3c876848a0", + "id": "ea3bdc7a-7c14-4ac1-955d-2540589fcfa6", "workPeriodId": "6ed56eb5-cadc-45f8-bbdf-3f408948c274", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 293.79, + "amount": 448.51, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:59.500Z", - "updatedAt": "2021-05-09T21:32:59.500Z" + "createdAt": "2021-05-09T21:33:00.899Z", + "updatedAt": "2021-05-09T21:33:00.899Z" }, { - "id": "ea3bdc7a-7c14-4ac1-955d-2540589fcfa6", + "id": "3bf29da2-581c-4f9c-8b0f-ff3c876848a0", "workPeriodId": "6ed56eb5-cadc-45f8-bbdf-3f408948c274", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, + "amount": 293.79, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:00.899Z", - "updatedAt": "2021-05-09T21:33:00.899Z" + "createdAt": "2021-05-09T21:32:59.500Z", + "updatedAt": "2021-05-09T21:32:59.500Z" } ] }, @@ -1585,28 +2136,28 @@ "updatedAt": "2021-05-09T21:48:03.740Z", "payments": [ { - "id": "5bdd5c22-d9b7-428c-b084-d0950a18bc37", + "id": "78641310-ad51-40a3-a0fd-fdd8d15455b9", "workPeriodId": "0c825dec-6e7b-4dde-943a-f3f8354219cc", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 57.79, + "amount": 417.42, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:49.123Z", - "updatedAt": "2021-05-09T21:30:49.123Z" + "createdAt": "2021-05-09T21:30:50.460Z", + "updatedAt": "2021-05-09T21:30:50.460Z" }, { - "id": "78641310-ad51-40a3-a0fd-fdd8d15455b9", + "id": "5bdd5c22-d9b7-428c-b084-d0950a18bc37", "workPeriodId": "0c825dec-6e7b-4dde-943a-f3f8354219cc", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 417.42, + "amount": 57.79, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:50.460Z", - "updatedAt": "2021-05-09T21:30:50.460Z" + "createdAt": "2021-05-09T21:30:49.123Z", + "updatedAt": "2021-05-09T21:30:49.123Z" } ] } @@ -1646,28 +2197,28 @@ "updatedAt": "2021-05-09T21:45:01.792Z", "payments": [ { - "id": "381af41e-6b0a-49fe-987f-1bcb03fda571", + "id": "1d2b92e8-194f-477a-97f8-e104056e6b10", "workPeriodId": "13f22f72-9240-43bb-ba0c-618d0b24ad8c", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 460.88, + "amount": 39.66, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:08.119Z", - "updatedAt": "2021-05-09T21:31:08.119Z" + "createdAt": "2021-05-09T21:31:09.459Z", + "updatedAt": "2021-05-09T21:31:09.459Z" }, { - "id": "1d2b92e8-194f-477a-97f8-e104056e6b10", + "id": "381af41e-6b0a-49fe-987f-1bcb03fda571", "workPeriodId": "13f22f72-9240-43bb-ba0c-618d0b24ad8c", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 39.66, + "amount": 460.88, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:09.459Z", - "updatedAt": "2021-05-09T21:31:09.459Z" + "createdAt": "2021-05-09T21:31:08.119Z", + "updatedAt": "2021-05-09T21:31:08.119Z" } ] }, @@ -1688,28 +2239,28 @@ "updatedAt": "2021-05-09T21:44:51.852Z", "payments": [ { - "id": "ef951baa-a007-48db-a658-495c6eeda9bc", + "id": "f0f85e56-6bf4-4e1f-a1ef-c31529efe4cd", "workPeriodId": "0cf74043-b432-41a5-99d9-83420a6ad8ef", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, - "status": "completed", + "amount": 466.42, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:59.323Z", - "updatedAt": "2021-05-09T21:30:59.323Z" + "createdAt": "2021-05-09T21:30:58.017Z", + "updatedAt": "2021-05-09T21:30:58.017Z" }, { - "id": "f0f85e56-6bf4-4e1f-a1ef-c31529efe4cd", + "id": "ef951baa-a007-48db-a658-495c6eeda9bc", "workPeriodId": "0cf74043-b432-41a5-99d9-83420a6ad8ef", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 466.42, - "status": "cancelled", + "amount": 448.51, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:58.017Z", - "updatedAt": "2021-05-09T21:30:58.017Z" + "createdAt": "2021-05-09T21:30:59.323Z", + "updatedAt": "2021-05-09T21:30:59.323Z" } ] }, @@ -1730,28 +2281,28 @@ "updatedAt": "2021-05-09T21:45:16.298Z", "payments": [ { - "id": "1cb5129e-8d92-4280-a946-8cb0f5757abc", + "id": "b576f845-7dea-4b56-b0de-6ce15fd2c245", "workPeriodId": "028287bf-6999-4fef-bdfa-1229b4e23ac1", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 460.88, + "amount": 477.97, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:33.549Z", - "updatedAt": "2021-05-09T21:30:33.549Z" + "createdAt": "2021-05-09T21:30:25.688Z", + "updatedAt": "2021-05-09T21:30:25.688Z" }, { - "id": "b576f845-7dea-4b56-b0de-6ce15fd2c245", + "id": "1cb5129e-8d92-4280-a946-8cb0f5757abc", "workPeriodId": "028287bf-6999-4fef-bdfa-1229b4e23ac1", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 477.97, + "amount": 460.88, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:25.688Z", - "updatedAt": "2021-05-09T21:30:25.688Z" + "createdAt": "2021-05-09T21:30:33.549Z", + "updatedAt": "2021-05-09T21:30:33.549Z" } ] }, @@ -1802,28 +2353,28 @@ "updatedAt": "2021-05-09T21:45:08.968Z", "payments": [ { - "id": "fa4c7e24-470f-4aef-a269-59b7e0b2bc05", + "id": "99e4bffb-90f8-411e-9a49-fc7779bd2c07", "workPeriodId": "32b977c9-386a-4159-a1c3-08169ee12f6e", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 460.88, - "status": "cancelled", + "amount": 416.38, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:56.152Z", - "updatedAt": "2021-05-09T21:31:56.152Z" + "createdAt": "2021-05-09T21:31:57.470Z", + "updatedAt": "2021-05-09T21:31:57.470Z" }, { - "id": "99e4bffb-90f8-411e-9a49-fc7779bd2c07", + "id": "fa4c7e24-470f-4aef-a269-59b7e0b2bc05", "workPeriodId": "32b977c9-386a-4159-a1c3-08169ee12f6e", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 416.38, - "status": "completed", + "amount": 460.88, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:57.470Z", - "updatedAt": "2021-05-09T21:31:57.470Z" + "createdAt": "2021-05-09T21:31:56.152Z", + "updatedAt": "2021-05-09T21:31:56.152Z" } ] } @@ -1893,7 +2444,7 @@ "updatedAt": "2021-05-09T21:46:39.040Z", "payments": [ { - "id": "d0ebcc96-70f2-4716-92d4-74e40af04387", + "id": "71b3b7d4-129c-4348-9ead-6f22eafa6db8", "workPeriodId": "355c7114-753a-4f99-b026-1d1430bf5530", "challengeId": "00000000-0000-0000-0000-000000000000", "amount": 416.38, @@ -1901,11 +2452,11 @@ "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:04.463Z", - "updatedAt": "2021-05-09T21:32:04.463Z" + "createdAt": "2021-05-09T21:32:05.827Z", + "updatedAt": "2021-05-09T21:32:05.827Z" }, { - "id": "71b3b7d4-129c-4348-9ead-6f22eafa6db8", + "id": "d0ebcc96-70f2-4716-92d4-74e40af04387", "workPeriodId": "355c7114-753a-4f99-b026-1d1430bf5530", "challengeId": "00000000-0000-0000-0000-000000000000", "amount": 416.38, @@ -1913,8 +2464,8 @@ "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:05.827Z", - "updatedAt": "2021-05-09T21:32:05.827Z" + "createdAt": "2021-05-09T21:32:04.463Z", + "updatedAt": "2021-05-09T21:32:04.463Z" } ] }, @@ -1995,28 +2546,28 @@ "updatedAt": "2021-05-09T21:46:44.896Z", "payments": [ { - "id": "d36afd1d-1f76-4f2d-b630-69bf85796496", + "id": "71750282-0ffe-46ed-b8c2-37e36c148833", "workPeriodId": "91fcf91f-b2cf-4909-8f03-b5efc0732b28", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 494.46, + "amount": 203.74, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:12.838Z", - "updatedAt": "2021-05-09T21:33:12.838Z" + "createdAt": "2021-05-09T21:33:14.202Z", + "updatedAt": "2021-05-09T21:33:14.202Z" }, { - "id": "71750282-0ffe-46ed-b8c2-37e36c148833", + "id": "d36afd1d-1f76-4f2d-b630-69bf85796496", "workPeriodId": "91fcf91f-b2cf-4909-8f03-b5efc0732b28", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 203.74, + "amount": 494.46, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:14.202Z", - "updatedAt": "2021-05-09T21:33:14.202Z" + "createdAt": "2021-05-09T21:33:12.838Z", + "updatedAt": "2021-05-09T21:33:12.838Z" } ] }, @@ -2051,6 +2602,4600 @@ ] } ] + }, + { + "id": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "projectId": 16714, + "userId": "4b00d029-c87b-47b2-bfe2-0ab80d8b5774", + "jobId": "fc0240f0-8c8f-40ce-a551-e83b45673098", + "status": "sourcing", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 106.51, + "customerRate": 296.66, + "rateType": "daily", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-25T13:10:26.146Z", + "updatedAt": "2021-05-30T11:41:11.436Z", + "workPeriods": [ + { + "id": "f5ab8f98-7e4b-4cf9-a1e5-6a15ab6bad91", + "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "userHandle": "nkumar2", + "projectId": 16714, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 63.94, + "customerRate": 16.07, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:12.251Z", + "updatedAt": "2021-05-30T16:02:51.923Z", + "payments": [] + }, + { + "id": "c20620e6-17de-41f5-8a0a-089683550b2f", + "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "userHandle": "nkumar2", + "projectId": 16714, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 248.87, + "customerRate": 217.32, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:12.260Z", + "updatedAt": "2021-05-30T16:03:39.019Z", + "payments": [] + }, + { + "id": "5c01c872-eb1e-4161-8e54-791f4ffe7bba", + "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "userHandle": "nkumar2", + "projectId": 16714, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 248.87, + "customerRate": 281.39, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:12.223Z", + "updatedAt": "2021-05-30T16:04:56.201Z", + "payments": [] + }, + { + "id": "1073cd3c-8fc3-4642-b71b-ed49e3628952", + "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "userHandle": "nkumar2", + "projectId": 16714, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 196.23, + "customerRate": 271.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:41:12.278Z", + "updatedAt": "2021-05-30T16:12:06.800Z", + "payments": [] + }, + { + "id": "1d912726-4d5e-4063-9651-bcf51fd4f42e", + "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "userHandle": "nkumar2", + "projectId": 16714, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 63.04, + "customerRate": 47.87, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:12.274Z", + "updatedAt": "2021-05-30T16:05:47.699Z", + "payments": [] + }, + { + "id": "15cbf62c-67d9-4f4a-92dc-546cb0afac32", + "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "userHandle": "nkumar2", + "projectId": 16714, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 295.62, + "customerRate": 235.7, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:41:12.216Z", + "updatedAt": "2021-05-30T16:12:06.801Z", + "payments": [] + } + ] + }, + { + "id": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "projectId": 17091, + "userId": "b870a229-e954-482d-b270-a39fcf2aec53", + "jobId": "fb8b92f6-4ffb-4ba6-8c38-c2d4a151f76b", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 4.07, + "customerRate": 132.43, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:53:14.352Z", + "updatedAt": "2021-05-30T11:48:17.788Z", + "workPeriods": [ + { + "id": "73224384-4ac4-402e-8c6e-e04c64ef34da", + "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "userHandle": "jimsun", + "projectId": 17091, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 8.1, + "customerRate": 155.91, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:18.557Z", + "updatedAt": "2021-05-30T16:11:38.205Z", + "payments": [] + }, + { + "id": "bcadfe78-7237-45e9-bd77-cba366ef9cf5", + "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "userHandle": "jimsun", + "projectId": 17091, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 216.18, + "customerRate": 262.91, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:18.549Z", + "updatedAt": "2021-05-30T16:03:44.298Z", + "payments": [] + }, + { + "id": "31436a53-4c2e-40fc-98f2-2b6e3e36aa2b", + "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "userHandle": "jimsun", + "projectId": 17091, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": null, + "memberRate": 104.85, + "customerRate": 225.57, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:18.565Z", + "updatedAt": "2021-05-30T16:11:38.208Z", + "payments": [] + }, + { + "id": "b04da80e-eff3-4be3-8849-f39f6af417b9", + "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "userHandle": "jimsun", + "projectId": 17091, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 131.12, + "customerRate": 270.74, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:18.561Z", + "updatedAt": "2021-05-30T16:03:59.366Z", + "payments": [] + }, + { + "id": "788dae21-92c8-4ae8-9e10-4e394f22cb73", + "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "userHandle": "jimsun", + "projectId": 17091, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 34.56, + "customerRate": 265.36, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:18.554Z", + "updatedAt": "2021-05-30T16:04:28.730Z", + "payments": [] + }, + { + "id": "eaffd373-e0f8-4798-ae42-24fb9bebeef3", + "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "userHandle": "jimsun", + "projectId": 17091, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 30.95, + "customerRate": 54.36, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:18.552Z", + "updatedAt": "2021-05-30T16:03:06.211Z", + "payments": [] + } + ] + }, + { + "id": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", + "projectId": 16781, + "userId": "7eea7c2f-5a46-4646-82bd-db4ac528378d", + "jobId": "ff3feeae-d4f7-457c-bff7-215be5efe2b8", + "status": "sourcing", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 61.33, + "customerRate": 196.21, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-12T10:49:14.209Z", + "updatedAt": "2021-05-30T11:48:22.088Z", + "workPeriods": [ + { + "id": "41217a15-4231-480c-91bc-492cbbe95113", + "resourceBookingId": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", + "userHandle": "ZeroChance", + "projectId": 16781, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 217.78, + "customerRate": 134.84, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:22.909Z", + "updatedAt": "2021-05-30T16:11:40.871Z", + "payments": [] + }, + { + "id": "cd2ff33c-70d9-4b47-a1f2-d3a32febb22d", + "resourceBookingId": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", + "userHandle": "ZeroChance", + "projectId": 16781, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 297.89, + "customerRate": 277.1, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:22.945Z", + "updatedAt": "2021-05-30T16:03:28.495Z", + "payments": [] + }, + { + "id": "354f7ab0-b9e3-4afd-a082-0ef0bc02ae44", + "resourceBookingId": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", + "userHandle": "ZeroChance", + "projectId": 16781, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 4, + "memberRate": 197.07, + "customerRate": 93.25, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:22.902Z", + "updatedAt": "2021-05-30T16:05:23.620Z", + "payments": [] + }, + { + "id": "e3edac07-0c38-4f5a-a62d-b1fcc037e6c9", + "resourceBookingId": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", + "userHandle": "ZeroChance", + "projectId": 16781, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 115.09, + "customerRate": 282.56, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:22.907Z", + "updatedAt": "2021-05-30T16:03:11.591Z", + "payments": [] + }, + { + "id": "4703c8d8-7e7b-4ee3-8cea-08c3d15de835", + "resourceBookingId": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", + "userHandle": "ZeroChance", + "projectId": 16781, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 175.62, + "customerRate": 101, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:22.912Z", + "updatedAt": "2021-05-30T16:05:12.956Z", + "payments": [] + } + ] + }, + { + "id": "d2f7dc2e-bed6-4549-b6c6-0616840782fb", + "projectId": 16762, + "userId": "595edbcd-f4d1-468e-a422-50bf61e2fa87", + "jobId": "fe481d1c-cf87-49c1-9370-695f9f754041", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 66, + "customerRate": 146.2, + "rateType": "daily", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-12T10:58:55.040Z", + "updatedAt": "2021-05-30T11:48:26.483Z", + "workPeriods": [ + { + "id": "f040a900-cd3e-4ebf-9a52-10db19e90e83", + "resourceBookingId": "d2f7dc2e-bed6-4549-b6c6-0616840782fb", + "userHandle": "sachin-kumar", + "projectId": 16762, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 178.01, + "customerRate": 282.9, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:27.249Z", + "updatedAt": "2021-05-30T16:02:59.987Z", + "payments": [] + }, + { + "id": "ecb9c0a7-0438-482d-9c8b-6ad6ffba2586", + "resourceBookingId": "d2f7dc2e-bed6-4549-b6c6-0616840782fb", + "userHandle": "sachin-kumar", + "projectId": 16762, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 148.65, + "customerRate": 131.63, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:27.266Z", + "updatedAt": "2021-05-30T16:03:04.382Z", + "payments": [] + }, + { + "id": "98f6ced4-7a27-4dd7-bf51-5bba41091f03", + "resourceBookingId": "d2f7dc2e-bed6-4549-b6c6-0616840782fb", + "userHandle": "sachin-kumar", + "projectId": 16762, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 69.98, + "customerRate": 245.82, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:27.256Z", + "updatedAt": "2021-05-30T16:04:13.737Z", + "payments": [] + }, + { + "id": "02281a71-46c3-4850-a243-737616ef78dc", + "resourceBookingId": "d2f7dc2e-bed6-4549-b6c6-0616840782fb", + "userHandle": "sachin-kumar", + "projectId": 16762, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 28.27, + "customerRate": 154.51, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:27.259Z", + "updatedAt": "2021-05-30T16:11:43.711Z", + "payments": [] + }, + { + "id": "248b0c82-8f49-482c-bd71-f3127acdefd8", + "resourceBookingId": "d2f7dc2e-bed6-4549-b6c6-0616840782fb", + "userHandle": "sachin-kumar", + "projectId": 16762, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 118.86, + "customerRate": 300.07, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:27.311Z", + "updatedAt": "2021-05-30T16:05:37.170Z", + "payments": [] + } + ] + }, + { + "id": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "projectId": 16870, + "userId": "b851684b-1071-47c3-8719-bdae96aa0e6d", + "jobId": "fed687e1-4257-48bb-806c-38712f9bf14f", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 115.29, + "customerRate": 278.45, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-04-29T09:50:13.905Z", + "updatedAt": "2021-05-30T11:48:15.196Z", + "workPeriods": [ + { + "id": "e95f0541-bc37-4c1e-a619-029753c1a69a", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 43.3, + "customerRate": 40.52, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:16.045Z", + "updatedAt": "2021-05-30T16:03:08.035Z", + "payments": [] + }, + { + "id": "cb6114ca-1624-4239-8243-aee05f8b5fe5", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 216.67, + "customerRate": 260.18, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:16.042Z", + "updatedAt": "2021-05-30T16:03:29.374Z", + "payments": [] + }, + { + "id": "f382c2c8-fe32-42f3-b960-29c345ab0264", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 185.3, + "customerRate": 203.04, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:16.004Z", + "updatedAt": "2021-05-30T16:11:36.370Z", + "payments": [] + }, + { + "id": "9215b576-8f67-4511-88f9-76000d6c8326", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 283.73, + "customerRate": 232.51, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:16.008Z", + "updatedAt": "2021-05-30T16:04:19.062Z", + "payments": [] + }, + { + "id": "333bcf25-25cc-4a4e-a6d7-2666a1326d68", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 162.3, + "customerRate": 132.28, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:16.001Z", + "updatedAt": "2021-05-30T16:05:26.263Z", + "payments": [] + } + ] + }, + { + "id": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "projectId": 17232, + "userId": "1ab93e53-71f6-4c50-ab48-9446229b6451", + "jobId": "ff76b81d-f49b-4019-b50e-c7932a818f19", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 18.4, + "customerRate": 84.88, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-17T13:12:55.459Z", + "updatedAt": "2021-05-30T11:48:11.067Z", + "workPeriods": [ + { + "id": "e84de57c-d131-4431-8e46-9452218d30e7", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 253.31, + "customerRate": 57.88, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:12.550Z", + "updatedAt": "2021-05-30T16:03:08.981Z", + "payments": [] + }, + { + "id": "97dabae3-74dd-45f6-ab83-53e4a828c4a6", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 297.19, + "customerRate": 280.07, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:12.488Z", + "updatedAt": "2021-05-30T16:11:34.545Z", + "payments": [] + }, + { + "id": "75c345fa-9e34-46a9-8cf0-46245495110d", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 236.08, + "customerRate": 26.72, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:12.586Z", + "updatedAt": "2021-05-30T16:04:33.475Z", + "payments": [] + }, + { + "id": "20242864-9dd6-4376-85fb-1402297e4597", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 214.14, + "customerRate": 67.44, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:12.497Z", + "updatedAt": "2021-05-30T16:05:41.521Z", + "payments": [] + }, + { + "id": "c0c613e3-845a-45c0-85fe-41c063d9df3d", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 33.79, + "customerRate": 282.9, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:12.493Z", + "updatedAt": "2021-05-30T16:11:34.546Z", + "payments": [] + }, + { + "id": "b53b6f48-0a07-434e-a03d-f5d9ab772e60", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 4, + "memberRate": 58.11, + "customerRate": 256.06, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:12.515Z", + "updatedAt": "2021-05-30T16:03:53.254Z", + "payments": [] + } + ] + }, + { + "id": "f667a667-6026-4d93-89bb-358aced982e5", + "projectId": 16870, + "userId": "2bba34d5-20e4-46d6-bfc1-05736b17afbb", + "jobId": "fed687e1-4257-48bb-806c-38712f9bf14f", + "status": "placed", + "startDate": "2021-01-03", + "endDate": "2021-02-03", + "memberRate": 61.4, + "customerRate": 114.05, + "rateType": "daily", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-04-29T09:28:04.405Z", + "updatedAt": "2021-05-30T11:48:13.500Z", + "workPeriods": [ + { + "id": "1dd649b8-f536-4285-989d-56c45f1fca4d", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": null, + "memberRate": 21.8, + "customerRate": 54.36, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:14.325Z", + "updatedAt": "2021-05-30T16:05:45.949Z", + "payments": [] + }, + { + "id": "d44b8f1a-46b7-43a8-afd6-d2d13bd02fa5", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 298.88, + "customerRate": 84.09, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:14.367Z", + "updatedAt": "2021-05-30T16:03:23.275Z", + "payments": [] + }, + { + "id": "7011b330-8509-4f60-a2db-c0f5c9b5837b", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 3, + "memberRate": 47.41, + "customerRate": 18.13, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:14.320Z", + "updatedAt": "2021-05-30T16:11:35.512Z", + "payments": [] + }, + { + "id": "8252cd13-4351-4a86-9315-521963f329f5", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 63.04, + "customerRate": 209, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:14.322Z", + "updatedAt": "2021-05-30T16:04:23.465Z", + "payments": [] + }, + { + "id": "259d8de1-1c56-49d0-936d-d5fcbdcc5a8a", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": null, + "memberRate": 162.12, + "customerRate": 99.32, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:14.314Z", + "updatedAt": "2021-05-30T16:05:34.486Z", + "payments": [] + } + ] + }, + { + "id": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "projectId": 17103, + "userId": "fa5f4dc4-2992-4066-b4cc-16ceb5d1c1b7", + "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 287.14, + "customerRate": 258.37, + "rateType": "daily", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-02T06:00:42.366Z", + "updatedAt": "2021-05-30T11:48:23.808Z", + "workPeriods": [ + { + "id": "b60da405-cee8-41c9-919b-98d9acfd9f74", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", + "projectId": 17103, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 271.31, + "customerRate": 195.92, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:24.607Z", + "updatedAt": "2021-05-30T16:03:52.385Z", + "payments": [] + }, + { + "id": "19110441-201e-449b-a350-661c50fb1387", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", + "projectId": 17103, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 30.12, + "customerRate": 84.76, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:24.639Z", + "updatedAt": "2021-05-30T16:05:49.510Z", + "payments": [] + }, + { + "id": "1f12ada9-d0e6-43df-abe5-8a78850b20b4", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", + "projectId": 17103, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 191.92, + "customerRate": 6.17, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:24.601Z", + "updatedAt": "2021-05-30T16:11:41.938Z", + "payments": [] + }, + { + "id": "1e94f892-ae25-4233-b9b0-81aa70c00b1e", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", + "projectId": 17103, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 109.93, + "customerRate": 271.9, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:24.604Z", + "updatedAt": "2021-05-30T16:05:45.069Z", + "payments": [] + }, + { + "id": "4d7f0350-b2f0-4f31-acc3-6555c3756fdd", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", + "projectId": 17103, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 70.84, + "customerRate": 207.33, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:24.637Z", + "updatedAt": "2021-05-30T16:05:08.370Z", + "payments": [] + } + ] + }, + { + "id": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "projectId": 16870, + "userId": "60d3e956-820b-4d59-a30b-9309b838fac5", + "jobId": "fe539bef-9119-4a8c-b7b0-915e7e3a3ba3", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 60.63, + "customerRate": 132.43, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-04-30T08:09:51.618Z", + "updatedAt": "2021-05-30T11:48:20.311Z", + "workPeriods": [ + { + "id": "6ad492cf-4d90-4608-8dd5-13aaafad12e2", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 131.12, + "customerRate": 18.28, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:21.194Z", + "updatedAt": "2021-05-30T16:11:39.976Z", + "payments": [] + }, + { + "id": "61134c36-5e69-469c-bc50-75648f7949ca", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 112.54, + "customerRate": 222.98, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:21.187Z", + "updatedAt": "2021-05-30T16:04:51.659Z", + "payments": [] + }, + { + "id": "f095424c-9a15-4f37-b8e4-1cd685f17451", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 200.17, + "customerRate": 260.87, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:21.198Z", + "updatedAt": "2021-05-30T16:11:39.977Z", + "payments": [] + }, + { + "id": "f79b0a83-5f72-4e9c-bffe-4ebf694db7f4", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 27.09, + "customerRate": 40.76, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:21.107Z", + "updatedAt": "2021-05-30T16:02:50.987Z", + "payments": [] + }, + { + "id": "c75ee2eb-a7c0-4eb8-83f9-860ce22d1b03", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 278.4, + "customerRate": 270.68, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:21.104Z", + "updatedAt": "2021-05-30T16:03:34.616Z", + "payments": [] + }, + { + "id": "b0aad7c5-bafb-4cae-90ca-832334505e9b", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 155.75, + "customerRate": 13.09, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:21.192Z", + "updatedAt": "2021-05-30T16:03:58.410Z", + "payments": [] + } + ] + }, + { + "id": "016312f6-72cf-486b-be8f-956ca4b2171e", + "projectId": 16706, + "userId": "4b00d029-c87b-47b2-bfe2-0ab80d8b5774", + "jobId": "fc2b006d-997b-49c3-a414-59ee54a48f9f", + "status": "sourcing", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 114.33, + "customerRate": 67.93, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-26T08:27:38.010Z", + "updatedAt": "2021-05-30T11:40:20.936Z", + "workPeriods": [ + { + "id": "6629d501-d17f-4261-a4d1-ed51d2a4b533", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 181.55, + "customerRate": 22.69, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:40:21.784Z", + "updatedAt": "2021-05-30T16:04:46.695Z", + "payments": [] + }, + { + "id": "42ca2389-e8a6-42c5-8a97-8d47531d2f23", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 41.26, + "customerRate": 286.6, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:40:21.766Z", + "updatedAt": "2021-05-30T16:05:14.728Z", + "payments": [] + }, + { + "id": "a2e2905a-efd7-45ba-a891-f0523b4b1351", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": null, + "memberRate": 114.59, + "customerRate": 180.76, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:40:21.794Z", + "updatedAt": "2021-05-30T16:04:08.375Z", + "payments": [] + }, + { + "id": "b3101b68-cc83-4a5c-aa33-0c5220e4b78f", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 280.73, + "customerRate": 7.02, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:40:21.780Z", + "updatedAt": "2021-05-30T16:12:19.637Z", + "payments": [] + }, + { + "id": "149f2401-f2c2-4e3f-98fe-44148820cd5e", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 3.94, + "customerRate": 88.91, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:40:21.757Z", + "updatedAt": "2021-05-30T16:12:19.636Z", + "payments": [] + }, + { + "id": "e002f23e-8358-4f49-8770-af19aa23708e", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 4, + "memberRate": 108.35, + "customerRate": 189.61, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:40:21.710Z", + "updatedAt": "2021-05-30T16:03:14.321Z", + "payments": [] + } + ] + }, + { + "id": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "projectId": 16899, + "userId": "5bc40e16-4fdb-40f1-93fe-de465789e1b2", + "jobId": "fe270791-bc24-4f6a-8c1b-b897f5d97d2f", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 271.93, + "customerRate": 102.37, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:45:15.939Z", + "updatedAt": "2021-05-30T11:48:39.054Z", + "workPeriods": [ + { + "id": "eeea5cf0-513c-4d1a-9318-a376aa86c28f", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 298.22, + "customerRate": 277.1, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:39.879Z", + "updatedAt": "2021-05-30T16:03:01.733Z", + "payments": [] + }, + { + "id": "ab2c5ad4-165f-48d2-bf1c-005b56e049ce", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 174.63, + "customerRate": 103.63, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:39.824Z", + "updatedAt": "2021-05-30T16:11:51.073Z", + "payments": [] + }, + { + "id": "aed553d8-4eb4-45a8-86f0-3c21f81c7570", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 19.08, + "customerRate": 101.27, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:39.870Z", + "updatedAt": "2021-05-30T16:04:00.297Z", + "payments": [] + }, + { + "id": "4c9a7d50-8014-4ff2-9867-98dc66e466ac", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 209.34, + "customerRate": 97.01, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:39.838Z", + "updatedAt": "2021-05-30T16:05:09.308Z", + "payments": [] + }, + { + "id": "d5c4716f-e26a-4d3f-a57d-2410d7537ecd", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": null, + "memberRate": 30.95, + "customerRate": 156.32, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:39.820Z", + "updatedAt": "2021-05-30T16:03:21.454Z", + "payments": [] + } + ] + }, + { + "id": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "projectId": 16870, + "userId": "2bba34d5-20e4-46d6-bfc1-05736b17afbb", + "jobId": "fe539bef-9119-4a8c-b7b0-915e7e3a3ba3", + "status": "closed", + "startDate": "2021-01-02", + "endDate": "2021-02-02", + "memberRate": 18.4, + "customerRate": 30.14, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T03:43:44.374Z", + "updatedAt": "2021-05-30T11:49:00.956Z", + "workPeriods": [ + { + "id": "b9768ae4-ae40-4bb9-8bc2-970c9f36f0bf", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 0, + "memberRate": 213.59, + "customerRate": 286.15, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:01.820Z", + "updatedAt": "2021-05-30T16:12:04.121Z", + "payments": [] + }, + { + "id": "77ac0f07-d4ae-463e-a9db-437623a29958", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 191.95, + "customerRate": 172.18, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:01.793Z", + "updatedAt": "2021-05-30T16:04:31.663Z", + "payments": [] + }, + { + "id": "1f5d3abe-fe2b-4961-831c-b7bbdf76da82", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 54.98, + "customerRate": 237.88, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:01.732Z", + "updatedAt": "2021-05-30T16:05:43.321Z", + "payments": [] + }, + { + "id": "1c526f5f-d9e0-4e16-8421-cee4e8154a3c", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 36.75, + "customerRate": 21.33, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:01.802Z", + "updatedAt": "2021-05-30T16:05:48.592Z", + "payments": [] + }, + { + "id": "b7bce7db-65dc-4447-8865-f6e8a84a867e", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 209.34, + "customerRate": 251.36, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:01.698Z", + "updatedAt": "2021-05-30T16:03:48.778Z", + "payments": [] + }, + { + "id": "a475c2fa-c5ce-4a30-bdd8-d4fad1e6308c", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 2, + "memberRate": 25.99, + "customerRate": 155.48, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:01.795Z", + "updatedAt": "2021-05-30T16:12:04.120Z", + "payments": [] + } + ] + }, + { + "id": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "projectId": 17290, + "userId": "0eaf032f-f376-47cc-b7aa-668685efac90", + "jobId": "fe600350-0a6d-4dac-922f-a6a7d285daa1", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 106.51, + "customerRate": 111.21, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-18T08:18:13.123Z", + "updatedAt": "2021-05-30T11:48:52.915Z", + "workPeriods": [ + { + "id": "e6b70714-8bd6-47ce-9e58-f04d3c25ee28", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 147.9, + "customerRate": 132.28, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:53.719Z", + "updatedAt": "2021-05-30T16:03:09.841Z", + "payments": [] + }, + { + "id": "b6c1f079-e5cb-46a0-a6bf-5988ec013c4c", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 22.66, + "customerRate": 265.36, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:53.776Z", + "updatedAt": "2021-05-30T16:03:49.745Z", + "payments": [] + }, + { + "id": "064d2511-9af6-4d6a-be4f-79eebacc6345", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 66.85, + "customerRate": 16.02, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:53.778Z", + "updatedAt": "2021-05-30T16:11:59.550Z", + "payments": [] + }, + { + "id": "c8c69543-1598-43e4-9ef6-8a569ebdf831", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 252.02, + "customerRate": 271.75, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:53.716Z", + "updatedAt": "2021-05-30T16:11:59.551Z", + "payments": [] + }, + { + "id": "0d237fa9-3fe9-48dc-82b8-7027edddc5a1", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 33.79, + "customerRate": 155.11, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:53.731Z", + "updatedAt": "2021-05-30T16:05:57.480Z", + "payments": [] + }, + { + "id": "7bc96a71-deda-4cde-b8b0-f809cea1398a", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 140.77, + "customerRate": 120.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:53.710Z", + "updatedAt": "2021-05-30T16:04:26.082Z", + "payments": [] + } + ] + }, + { + "id": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "projectId": 17290, + "userId": "1f6ca39c-0620-4de0-9bb2-d64d4ce26b42", + "jobId": "fe600350-0a6d-4dac-922f-a6a7d285daa1", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 4.07, + "customerRate": 296.66, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-18T07:05:53.664Z", + "updatedAt": "2021-05-30T11:48:33.177Z", + "workPeriods": [ + { + "id": "e29735b3-cf81-4877-8fd5-6d346a1824f0", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 8.1, + "customerRate": 273.89, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:33.942Z", + "updatedAt": "2021-05-30T16:03:12.511Z", + "payments": [] + }, + { + "id": "df0b3604-7ee9-4862-a8d1-abe8a2142f77", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 154, + "customerRate": 244.09, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:33.954Z", + "updatedAt": "2021-05-30T16:03:15.193Z", + "payments": [] + }, + { + "id": "9a480e44-2026-4327-a220-715ace30743e", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 248.36, + "customerRate": 257.04, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:33.952Z", + "updatedAt": "2021-05-30T16:04:12.811Z", + "payments": [] + }, + { + "id": "b6147962-6666-4534-8b73-0c7f9a7052e8", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 126.28, + "customerRate": 4.22, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:33.959Z", + "updatedAt": "2021-05-30T16:11:47.528Z", + "payments": [] + }, + { + "id": "47ac2474-d9e9-416d-afa8-fea8fb4f2a6c", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 28.27, + "customerRate": 256.06, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:33.940Z", + "updatedAt": "2021-05-30T16:05:12.030Z", + "payments": [] + } + ] + }, + { + "id": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "projectId": 16781, + "userId": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "jobId": "ff3feeae-d4f7-457c-bff7-215be5efe2b8", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 54.02, + "customerRate": 217.99, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-12T15:02:15.210Z", + "updatedAt": "2021-05-30T11:48:59.149Z", + "workPeriods": [ + { + "id": "d062e2fe-2446-4fb5-b0b3-0577cd57fd7c", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 122.15, + "customerRate": 115.26, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:59.909Z", + "updatedAt": "2021-05-30T16:03:24.198Z", + "payments": [] + }, + { + "id": "bdd60068-9e8d-4c43-8440-48a066ba4396", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 15.41, + "customerRate": 55.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:59.921Z", + "updatedAt": "2021-05-30T16:12:03.178Z", + "payments": [] + }, + { + "id": "a0eec246-3b4d-41b6-9d54-b892513bd727", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 231.38, + "customerRate": 270.68, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:00.007Z", + "updatedAt": "2021-05-30T16:04:10.173Z", + "payments": [] + }, + { + "id": "752f3cf3-a9c2-487c-93ae-22d21af10403", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 66.85, + "customerRate": 42.45, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:00.041Z", + "updatedAt": "2021-05-30T16:04:34.403Z", + "payments": [] + }, + { + "id": "5be5a577-32cd-4557-8d11-bb5723dd7be2", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 158.5, + "customerRate": 299.34, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:59.998Z", + "updatedAt": "2021-05-30T16:04:57.065Z", + "payments": [] + }, + { + "id": "e9ff7b7f-05ec-45f9-a09e-b2c24017b59b", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 34.56, + "customerRate": 175.32, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:59.974Z", + "updatedAt": "2021-05-30T16:12:03.180Z", + "payments": [] + } + ] + }, + { + "id": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "projectId": 17363, + "userId": "c40cdb0a-4fac-4ca1-8052-d92001858887", + "jobId": "fd48d96e-b0f2-43b7-8a48-f4fa194d6bc8", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 235.26, + "customerRate": 84.88, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "71c5e6a8-51d9-4fb5-91ce-d974642531af", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-28T04:41:39.728Z", + "updatedAt": "2021-05-30T11:48:34.793Z", + "workPeriods": [ + { + "id": "94a897e9-6291-4206-a0b1-74c35ff06a6e", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": null, + "memberRate": 214.34, + "customerRate": 16.02, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:35.596Z", + "updatedAt": "2021-05-30T16:04:17.285Z", + "payments": [] + }, + { + "id": "2570737c-02b4-4b90-b692-45dcc5774215", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 34.23, + "customerRate": 257.04, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:35.576Z", + "updatedAt": "2021-05-30T16:11:48.431Z", + "payments": [] + }, + { + "id": "789b83d2-3278-4c90-a5c1-3aa96e61db4b", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 181.55, + "customerRate": 97.01, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:35.600Z", + "updatedAt": "2021-05-30T16:04:27.884Z", + "payments": [] + }, + { + "id": "764fec45-842e-4d06-b009-29ba4c9c116c", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 197.07, + "customerRate": 39.93, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:35.588Z", + "updatedAt": "2021-05-30T16:04:32.547Z", + "payments": [] + }, + { + "id": "00fa978f-e2c4-4cab-8da9-e6b0dce80258", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 34.07, + "customerRate": 287.29, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:35.585Z", + "updatedAt": "2021-05-30T16:06:04.649Z", + "payments": [] + }, + { + "id": "daa54d7d-84f5-4f62-b58c-4c09ec26dd1a", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 297.19, + "customerRate": 32.3, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:35.581Z", + "updatedAt": "2021-05-30T16:11:48.432Z", + "payments": [] + } + ] + }, + { + "id": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "projectId": 17103, + "userId": "d8e11333-af08-4149-a270-b355001b44e7", + "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 240.84, + "customerRate": 100.25, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T12:20:21.613Z", + "updatedAt": "2021-05-30T11:48:28.143Z", + "workPeriods": [ + { + "id": "d0571318-a0be-4263-bfe9-eba783ba8957", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 205.68, + "customerRate": 154.12, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:28.918Z", + "updatedAt": "2021-05-30T16:03:25.073Z", + "payments": [] + }, + { + "id": "9fd57023-518e-42e3-a998-6098ae49a3fb", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 4, + "memberRate": 202.33, + "customerRate": 1.49, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:28.905Z", + "updatedAt": "2021-05-30T16:04:11.041Z", + "payments": [] + }, + { + "id": "b473e949-0a6e-452c-b626-6cc79ee61d1c", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 146.28, + "customerRate": 145.74, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:28.924Z", + "updatedAt": "2021-05-30T16:03:54.140Z", + "payments": [] + }, + { + "id": "b952458d-12a9-4398-b549-3fe44d3814dd", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 214.34, + "customerRate": 167.08, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:28.922Z", + "updatedAt": "2021-05-30T16:11:44.684Z", + "payments": [] + }, + { + "id": "f206743e-f797-40a8-8dee-634fb0f08e08", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 184.32, + "customerRate": 255.03, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:28.873Z", + "updatedAt": "2021-05-30T16:02:57.423Z", + "payments": [] + }, + { + "id": "ef0f5c89-c979-4504-9b20-54b110a1495a", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 166.73, + "customerRate": 189.61, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:28.915Z", + "updatedAt": "2021-05-30T16:11:44.683Z", + "payments": [] + } + ] + }, + { + "id": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "projectId": 16899, + "userId": "47034de0-698d-4e1b-a10b-ae4b8c59288e", + "jobId": "fe270791-bc24-4f6a-8c1b-b897f5d97d2f", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 4.07, + "customerRate": 258.37, + "rateType": "monthly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:36:02.161Z", + "updatedAt": "2021-05-30T11:48:31.516Z", + "workPeriods": [ + { + "id": "0ae1abc8-3e8d-4eea-933e-ab3cfa7c6826", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 122.15, + "customerRate": 25.64, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:32.269Z", + "updatedAt": "2021-05-30T16:11:46.620Z", + "payments": [] + }, + { + "id": "97fd7b67-0cd7-47fb-9017-7a7b653ef940", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 298.22, + "customerRate": 271.9, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:32.256Z", + "updatedAt": "2021-05-30T16:04:14.644Z", + "payments": [] + }, + { + "id": "3fe04a0d-62ff-4060-935a-4f294ee19fac", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 280.11, + "customerRate": 195.99, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:32.237Z", + "updatedAt": "2021-05-30T16:11:46.621Z", + "payments": [] + }, + { + "id": "a3daf653-c87c-451b-acfc-508ef8364fbb", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 207.81, + "customerRate": 297.59, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:32.273Z", + "updatedAt": "2021-05-30T16:04:05.668Z", + "payments": [] + }, + { + "id": "15337715-0c4a-4036-a1d3-5aaa14a214af", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 104.85, + "customerRate": 260.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:32.252Z", + "updatedAt": "2021-05-30T16:05:51.388Z", + "payments": [] + }, + { + "id": "304a0ba1-2534-46c0-b2e9-9d85936755df", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 129.72, + "customerRate": 93.25, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:32.234Z", + "updatedAt": "2021-05-30T16:05:28.904Z", + "payments": [] + } + ] + }, + { + "id": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "projectId": 16706, + "userId": "149c9ad0-f5d7-4192-8c61-f634f6120816", + "jobId": "fc2b006d-997b-49c3-a414-59ee54a48f9f", + "status": "sourcing", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 201.77, + "customerRate": 84.88, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-26T08:27:38.944Z", + "updatedAt": "2021-05-30T11:41:01.828Z", + "workPeriods": [ + { + "id": "b21eed94-dce5-42e1-9734-155594773222", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 186.09, + "customerRate": 37.15, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:02.603Z", + "updatedAt": "2021-05-30T16:03:56.759Z", + "payments": [] + }, + { + "id": "85201917-77e0-41d1-8c76-66eefcbc23e0", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 143.95, + "customerRate": 7.02, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:41:02.617Z", + "updatedAt": "2021-05-30T16:11:54.733Z", + "payments": [] + }, + { + "id": "56105e62-d690-484c-9fe8-b6d18671c9ac", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 127.23, + "customerRate": 162.5, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:02.600Z", + "updatedAt": "2021-05-30T16:04:59.664Z", + "payments": [] + }, + { + "id": "5d6c9cf8-a6bc-43b9-af35-67812fc10db9", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 140.77, + "customerRate": 161.96, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:02.595Z", + "updatedAt": "2021-05-30T16:04:54.450Z", + "payments": [] + }, + { + "id": "34ab1933-c9a3-4087-b220-3716d3729703", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 202.33, + "customerRate": 140.66, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:02.645Z", + "updatedAt": "2021-05-30T16:05:24.577Z", + "payments": [] + } + ] + }, + { + "id": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "projectId": 16714, + "userId": "bde2cf99-b290-40cd-a064-9b6bb7e54bea", + "jobId": "fc0240f0-8c8f-40ce-a551-e83b45673098", + "status": "sourcing", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 115.29, + "customerRate": 84.88, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-26T08:16:00.294Z", + "updatedAt": "2021-05-30T11:48:40.735Z", + "workPeriods": [ + { + "id": "ae316c25-ec04-4bc1-b209-798b69ed5250", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 34.07, + "customerRate": 55.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:41.592Z", + "updatedAt": "2021-05-30T16:04:01.125Z", + "payments": [] + }, + { + "id": "7501c035-46c4-4704-973a-595145bd2d17", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 114.76, + "customerRate": 268.61, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:41.588Z", + "updatedAt": "2021-05-30T16:11:52.009Z", + "payments": [] + }, + { + "id": "519b4818-8e39-4d4c-bcc6-3486816a4bc8", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 173.56, + "customerRate": 140.66, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:41.595Z", + "updatedAt": "2021-05-30T16:05:04.624Z", + "payments": [] + }, + { + "id": "13444e2f-1254-40fc-a7d9-08ca49fc7239", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 276.5, + "customerRate": 287.29, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:41.597Z", + "updatedAt": "2021-05-30T16:05:53.017Z", + "payments": [] + }, + { + "id": "66c66f68-3882-4521-9c4e-ce08e6b47ca0", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 4, + "memberRate": 43.3, + "customerRate": 72.49, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:41.579Z", + "updatedAt": "2021-05-30T16:04:44.142Z", + "payments": [] + } + ] + }, + { + "id": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "projectId": 16805, + "userId": "13330208-ab10-4ca3-9fd1-a132fbf7ac4e", + "jobId": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 4.79, + "customerRate": 146.2, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-19T11:04:22.566Z", + "updatedAt": "2021-05-30T11:48:46.861Z", + "workPeriods": [ + { + "id": "f2a85b87-8e75-4db9-b29c-a3c170101ed6", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", + "projectId": 16805, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 104.6, + "customerRate": 217.32, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:47.631Z", + "updatedAt": "2021-05-30T16:02:54.605Z", + "payments": [] + }, + { + "id": "e6801711-327e-4387-a7f0-d592b49b1ba3", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", + "projectId": 16805, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 54.82, + "customerRate": 191.42, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:47.726Z", + "updatedAt": "2021-05-30T16:03:10.690Z", + "payments": [] + }, + { + "id": "cd44a405-c77a-4f6d-95a7-c10de740eb29", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", + "projectId": 16805, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 70.98, + "customerRate": 107.72, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:47.728Z", + "updatedAt": "2021-05-30T16:03:27.679Z", + "payments": [] + }, + { + "id": "6565d172-61c4-4357-a01f-b8e6bf179e2f", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", + "projectId": 16805, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 172.94, + "customerRate": 67.44, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:47.638Z", + "updatedAt": "2021-05-30T16:11:55.746Z", + "payments": [] + }, + { + "id": "f7c8a1e8-76c6-4eee-bb99-15cce5cb8b1f", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", + "projectId": 16805, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 219.84, + "customerRate": 122.72, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:47.648Z", + "updatedAt": "2021-05-30T16:11:55.747Z", + "payments": [] + }, + { + "id": "a71105ef-a2b8-4163-b2de-9ddcaa8329bb", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", + "projectId": 16805, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 252.02, + "customerRate": 219.79, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:47.731Z", + "updatedAt": "2021-05-30T16:04:02.955Z", + "payments": [] + } + ] + }, + { + "id": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "projectId": 17232, + "userId": "8e6bfd51-fd78-45fa-9234-172976168f29", + "jobId": "ff76b81d-f49b-4019-b50e-c7932a818f19", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 225.34, + "customerRate": 170.64, + "rateType": "monthly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-18T04:01:23.725Z", + "updatedAt": "2021-05-30T11:48:44.244Z", + "workPeriods": [ + { + "id": "eeda07cc-8a10-4337-ab8d-e1107e4072a0", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 204.06, + "customerRate": 143.1, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:45.002Z", + "updatedAt": "2021-05-30T16:03:02.635Z", + "payments": [] + }, + { + "id": "7aaa313f-62be-4483-80b2-87a95b4a0b8c", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 185.82, + "customerRate": 286.29, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:45.006Z", + "updatedAt": "2021-05-30T16:04:26.995Z", + "payments": [] + }, + { + "id": "c0568bd9-1523-494d-bb3b-4b7a89026de9", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 248.86, + "customerRate": 271.75, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:45.074Z", + "updatedAt": "2021-05-30T16:11:53.758Z", + "payments": [] + }, + { + "id": "4f7b2806-cdc4-40de-979e-82573123b1ce", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 162.88, + "customerRate": 11.72, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:45.076Z", + "updatedAt": "2021-05-30T16:05:06.523Z", + "payments": [] + }, + { + "id": "3a652bb3-69b2-4e9b-b6e7-804568a9a76b", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 54.97, + "customerRate": 167.68, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:45.012Z", + "updatedAt": "2021-05-30T16:05:20.251Z", + "payments": [] + } + ] + }, + { + "id": "905654a2-e07d-47a3-b577-c03d100bc94a", + "projectId": 17300, + "userId": "6719d9dc-beca-4731-a4be-a214152ccadf", + "jobId": "fd13ad99-f16a-4362-9274-80f5f38895c3", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 66, + "customerRate": 132.43, + "rateType": "daily", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-19T07:40:44.211Z", + "updatedAt": "2021-05-30T11:48:42.523Z", + "workPeriods": [ + { + "id": "6ca0c12a-eecb-4d12-b40d-89a19f2c38ad", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 216.67, + "customerRate": 281.91, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:43.331Z", + "updatedAt": "2021-05-30T16:11:52.856Z", + "payments": [] + }, + { + "id": "92f9e88b-bc75-4934-9c3f-cdb3a846e545", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 85.45, + "customerRate": 273.89, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:43.339Z", + "updatedAt": "2021-05-30T16:04:18.164Z", + "payments": [] + }, + { + "id": "bc7af7e8-873a-4ddf-a3a5-8b5f0db41827", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": null, + "memberRate": 239.68, + "customerRate": 180.76, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:43.254Z", + "updatedAt": "2021-05-30T16:03:45.211Z", + "payments": [] + }, + { + "id": "11084a75-bae7-4c93-a2fc-44ff691b6ded", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 79.89, + "customerRate": 174.2, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:43.335Z", + "updatedAt": "2021-05-30T16:05:54.898Z", + "payments": [] + }, + { + "id": "6c3a492a-d2bf-4740-91b0-0d877b044268", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 66.85, + "customerRate": 154.65, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:43.275Z", + "updatedAt": "2021-05-30T16:04:41.476Z", + "payments": [] + } + ] + }, + { + "id": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "projectId": 16718, + "userId": "a953dce3-8dd3-413f-b253-0ca76ff59f36", + "jobId": "fb2f5f9b-5874-4dcd-af94-727fc0409760", + "status": "sourcing", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 269.46, + "customerRate": 138.32, + "rateType": "monthly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-26T08:24:56.376Z", + "updatedAt": "2021-05-30T11:48:51.233Z", + "workPeriods": [ + { + "id": "0ed78625-e5f0-4f05-b326-632a002d150a", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 46.62, + "customerRate": 76.96, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:52.006Z", + "updatedAt": "2021-05-30T16:11:58.578Z", + "payments": [] + }, + { + "id": "279989ed-1e9e-45e9-bdb8-26c03d396344", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 173.64, + "customerRate": 160.37, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:51.981Z", + "updatedAt": "2021-05-30T16:05:33.514Z", + "payments": [] + }, + { + "id": "31530732-4c7a-429c-9384-c219f16590fa", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 239.68, + "customerRate": 84.09, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:51.988Z", + "updatedAt": "2021-05-30T16:05:27.123Z", + "payments": [] + }, + { + "id": "91065330-b979-4b3c-b084-0e14b6be6740", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 191.95, + "customerRate": 260.09, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:51.984Z", + "updatedAt": "2021-05-30T16:04:19.946Z", + "payments": [] + }, + { + "id": "06187a37-d29a-4bdb-bcb1-e0e7f57eec4a", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 124.66, + "customerRate": 219.67, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:51.978Z", + "updatedAt": "2021-05-30T16:06:00.996Z", + "payments": [] + } + ] + }, + { + "id": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "projectId": 16870, + "userId": "cc959274-bb53-4612-a4f4-af62496b026c", + "jobId": "fe8da845-5313-496f-b859-9824bd06a0db", + "status": "placed", + "startDate": "2021-01-12", + "endDate": "2021-02-12", + "memberRate": 240.84, + "customerRate": 188.33, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:30:37.276Z", + "updatedAt": "2021-05-30T11:48:54.737Z", + "workPeriods": [ + { + "id": "c44339a8-5562-493d-9e59-02fded34dadd", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 6.22, + "customerRate": 62.03, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:55.482Z", + "updatedAt": "2021-05-30T16:03:38.050Z", + "payments": [] + }, + { + "id": "6e8627d9-f3b9-4e56-ba48-0d4cd0572beb", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 21.58, + "customerRate": 72.49, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:55.537Z", + "updatedAt": "2021-05-30T16:04:39.704Z", + "payments": [] + }, + { + "id": "20b18eeb-a78f-4ff2-8a3b-fbd1cfba567c", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": null, + "memberRate": 41.26, + "customerRate": 286.29, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:55.526Z", + "updatedAt": "2021-05-30T16:05:40.643Z", + "payments": [] + }, + { + "id": "36abc507-0e01-46e3-ab78-52c0e8f848b1", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 102.8, + "customerRate": 149.44, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:55.539Z", + "updatedAt": "2021-05-30T16:05:22.767Z", + "payments": [] + }, + { + "id": "4b8cc238-bb26-4fbf-ab74-a86c1d9a47ce", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 4, + "memberRate": 96.7, + "customerRate": 13.09, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:55.532Z", + "updatedAt": "2021-05-30T16:12:00.505Z", + "payments": [] + } + ] + }, + { + "id": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "projectId": 16870, + "userId": "acdf9ebe-8358-4bd3-9374-1d86cf27e5f4", + "jobId": "fe8da845-5313-496f-b859-9824bd06a0db", + "status": "placed", + "startDate": "2021-01-21", + "endDate": "2021-02-21", + "memberRate": 114.33, + "customerRate": 258.37, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:30:59.306Z", + "updatedAt": "2021-05-30T11:48:56.459Z", + "workPeriods": [ + { + "id": "77dcf745-bd59-40d8-b563-75a4d5354d29", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-02-14", + "endDate": "2021-02-20", + "daysWorked": null, + "memberRate": 158.66, + "customerRate": 19.76, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:57.208Z", + "updatedAt": "2021-05-30T16:04:30.745Z", + "payments": [] + }, + { + "id": "97cb1bad-772a-4f1e-a5a3-f0b19ae766f2", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-02-21", + "endDate": "2021-02-27", + "daysWorked": 0, + "memberRate": 69.43, + "customerRate": 22.82, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:57.210Z", + "updatedAt": "2021-05-30T16:12:01.358Z", + "payments": [] + }, + { + "id": "4c626a59-e591-4e7a-88cb-1d601b9b8493", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 10.15, + "customerRate": 213.97, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:57.302Z", + "updatedAt": "2021-05-30T16:05:10.190Z", + "payments": [] + }, + { + "id": "55b66454-3a29-4163-9d97-7ecd2e805f71", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 2, + "memberRate": 230.66, + "customerRate": 193.93, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:57.225Z", + "updatedAt": "2021-05-30T16:12:01.359Z", + "payments": [] + }, + { + "id": "1fa1f111-6574-47b0-8d12-6832541d496c", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": null, + "memberRate": 104.6, + "customerRate": 62.03, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:57.244Z", + "updatedAt": "2021-05-30T16:05:42.410Z", + "payments": [] + }, + { + "id": "5bc5686b-95b3-49d7-9c8e-50f1dfdcb82e", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 165.75, + "customerRate": 262.91, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:57.306Z", + "updatedAt": "2021-05-30T16:04:57.957Z", + "payments": [] + } + ] + }, + { + "id": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "projectId": 16762, + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "jobId": "fe481d1c-cf87-49c1-9370-695f9f754041", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 61.33, + "customerRate": 84.88, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-12T10:53:32.373Z", + "updatedAt": "2021-05-30T11:48:49.532Z", + "workPeriods": [ + { + "id": "a154b1fb-06d3-4cfd-97d3-0a810a1c4317", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 202.33, + "customerRate": 128, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:50.279Z", + "updatedAt": "2021-05-30T16:04:09.261Z", + "payments": [] + }, + { + "id": "a35207bd-ac1d-4539-b7bb-7a923c8a6f7f", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 294.23, + "customerRate": 142.66, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:50.330Z", + "updatedAt": "2021-05-30T16:11:57.609Z", + "payments": [] + }, + { + "id": "3ad03850-ebe9-4227-8b30-1303b20bbd31", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 157.6, + "customerRate": 40.76, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:50.292Z", + "updatedAt": "2021-05-30T16:05:19.240Z", + "payments": [] + }, + { + "id": "2ea4bffd-2519-422f-8baa-a0f74b3b398b", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 218.59, + "customerRate": 195.92, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:50.340Z", + "updatedAt": "2021-05-30T16:05:30.731Z", + "payments": [] + }, + { + "id": "b34361ca-eb0e-47f7-86d9-3bccbb6839d5", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": null, + "memberRate": 238.31, + "customerRate": 11.09, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:50.288Z", + "updatedAt": "2021-05-30T16:03:55.011Z", + "payments": [] + }, + { + "id": "e1099a6a-7c6b-465d-bb1b-517c3fbd06f1", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 105.58, + "customerRate": 297.59, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:50.297Z", + "updatedAt": "2021-05-30T16:11:57.610Z", + "payments": [] + } + ] + }, + { + "id": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "projectId": 16805, + "userId": "3797d69c-0bf1-421e-b086-81e36ec1f929", + "jobId": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 1.59, + "customerRate": 170.64, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-28T05:15:29.662Z", + "updatedAt": "2021-05-30T11:48:37.381Z", + "workPeriods": [ + { + "id": "33b3b539-5741-49af-a700-fa8e9bd4abba", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 280.11, + "customerRate": 111.64, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:38.136Z", + "updatedAt": "2021-05-30T16:05:25.424Z", + "payments": [] + }, + { + "id": "eca90916-ded1-49d5-8beb-582bba178dd9", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 65.17, + "customerRate": 4.22, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:38.215Z", + "updatedAt": "2021-05-30T16:11:50.205Z", + "payments": [] + }, + { + "id": "2f176676-60f7-4d27-bb79-d1183eb0b7e0", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 196.76, + "customerRate": 42.45, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:38.152Z", + "updatedAt": "2021-05-30T16:05:29.869Z", + "payments": [] + }, + { + "id": "0ac738d8-03be-4ba4-a86b-2a1f65666cd5", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 188.95, + "customerRate": 122.72, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:38.220Z", + "updatedAt": "2021-05-30T16:05:59.284Z", + "payments": [] + }, + { + "id": "b180bd57-30b3-4092-affc-c306401edd7d", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 4, + "memberRate": 126.33, + "customerRate": 160.37, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:38.150Z", + "updatedAt": "2021-05-30T16:03:57.617Z", + "payments": [] + } + ] + }, + { + "id": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "projectId": 16739, + "userId": "3ed9015f-09d8-4173-bfcd-5dcc60c52060", + "jobId": "fc5ba131-566f-46fe-8501-79c593241896", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 61.33, + "customerRate": 114.05, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-17T13:41:56.896Z", + "updatedAt": "2021-05-30T11:48:29.760Z", + "workPeriods": [ + { + "id": "7d495eed-d042-4a96-beed-dc2f2c1054c1", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 245.61, + "customerRate": 158.61, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:30.527Z", + "updatedAt": "2021-05-30T16:04:25.202Z", + "payments": [] + }, + { + "id": "7468173d-6d92-4560-802e-6329ab656754", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 3.94, + "customerRate": 131.83, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:30.544Z", + "updatedAt": "2021-05-30T16:04:36.153Z", + "payments": [] + }, + { + "id": "1dc38edd-3b56-4ed3-ae6c-ea2527076b32", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 43.7, + "customerRate": 154.12, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:30.606Z", + "updatedAt": "2021-05-30T16:05:46.841Z", + "payments": [] + }, + { + "id": "212909c4-b1e9-4d12-b2ca-4175ccbb2d7f", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 85.9, + "customerRate": 134.84, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:30.530Z", + "updatedAt": "2021-05-30T16:11:45.643Z", + "payments": [] + }, + { + "id": "d4dffbb9-2224-4429-9d7e-4bd9d33dba70", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 4.63, + "customerRate": 152.35, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:30.547Z", + "updatedAt": "2021-05-30T16:03:22.384Z", + "payments": [] + }, + { + "id": "b6b60c49-615a-4367-b644-af68485b4293", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 18.46, + "customerRate": 171.14, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:30.540Z", + "updatedAt": "2021-05-30T16:11:45.644Z", + "payments": [] + } + ] + }, + { + "id": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "projectId": 17324, + "userId": "9807980a-a9e4-4f24-a48b-311fcdbf1f47", + "jobId": "fefd2618-9b66-4431-9874-1d02d7a37d90", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 287.14, + "customerRate": 146.2, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-27T05:05:05.125Z", + "updatedAt": "2021-05-30T11:49:06.302Z", + "workPeriods": [ + { + "id": "11d7db8c-b4a9-47b0-b24a-e45d4dc5fae4", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": null, + "memberRate": 85.95, + "customerRate": 178.12, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:07.103Z", + "updatedAt": "2021-05-30T16:05:53.988Z", + "payments": [] + }, + { + "id": "d711870a-4f78-431b-b5b5-ae5157999a0c", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 58.11, + "customerRate": 143.99, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:07.137Z", + "updatedAt": "2021-05-30T16:03:20.574Z", + "payments": [] + }, + { + "id": "d0266458-f9d2-42db-a716-6f114b4a0be0", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 36.75, + "customerRate": 269.78, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:07.095Z", + "updatedAt": "2021-05-30T16:03:25.900Z", + "payments": [] + }, + { + "id": "248de422-69c3-4c5b-8919-ba18113d0350", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 69.67, + "customerRate": 193.93, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:07.106Z", + "updatedAt": "2021-05-30T16:05:36.306Z", + "payments": [] + }, + { + "id": "6aabe458-6e77-4fbd-9092-d811e7bbd21d", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 126.28, + "customerRate": 213.99, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:07.144Z", + "updatedAt": "2021-05-30T16:12:08.558Z", + "payments": [] + }, + { + "id": "662931c7-09a4-43d9-a838-cb275296e818", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 177.96, + "customerRate": 101.95, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:07.100Z", + "updatedAt": "2021-05-30T16:12:08.560Z", + "payments": [] + } + ] + }, + { + "id": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "projectId": 17103, + "userId": "8fe0c1c3-e63e-4047-9854-01f03b166bd8", + "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "status": "closed", + "startDate": "2021-01-02", + "endDate": "2021-02-02", + "memberRate": 85.22, + "customerRate": 170.64, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T10:25:45.827Z", + "updatedAt": "2021-05-30T11:49:17.657Z", + "workPeriods": [ + { + "id": "0466ddf6-83ba-41ee-b299-4abb2b5f8a3b", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 0, + "memberRate": 191.67, + "customerRate": 40.76, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:18.405Z", + "updatedAt": "2021-05-30T16:12:15.134Z", + "payments": [] + }, + { + "id": "bd92f07b-4b57-4486-9101-254578cf32f8", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 282.2, + "customerRate": 177.54, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:18.505Z", + "updatedAt": "2021-05-30T16:03:42.520Z", + "payments": [] + }, + { + "id": "9c976d1a-f395-4889-ac9b-38846a083dcb", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 158.66, + "customerRate": 158.21, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:18.500Z", + "updatedAt": "2021-05-30T16:04:11.932Z", + "payments": [] + }, + { + "id": "62bf7ac9-bea9-4f96-8a28-2a3a8dbbc48f", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 21.58, + "customerRate": 10.21, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:18.439Z", + "updatedAt": "2021-05-30T16:04:50.801Z", + "payments": [] + }, + { + "id": "05fb419d-927c-4264-b346-905ba7a55f49", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 2, + "memberRate": 294.55, + "customerRate": 40.52, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:18.492Z", + "updatedAt": "2021-05-30T16:12:15.133Z", + "payments": [] + }, + { + "id": "c4b535c4-0c6f-4420-930e-0103aea68057", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 4, + "memberRate": 42.79, + "customerRate": 298.27, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:18.490Z", + "updatedAt": "2021-05-30T16:03:36.317Z", + "payments": [] + } + ] + }, + { + "id": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "projectId": 17091, + "userId": "de029f4b-f07b-4f8e-bc58-d928b8d8d289", + "jobId": "fb8b92f6-4ffb-4ba6-8c38-c2d4a151f76b", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 271.93, + "customerRate": 258.37, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:46:05.754Z", + "updatedAt": "2021-05-30T11:49:14.166Z", + "workPeriods": [ + { + "id": "f234f6bb-a90f-4f2d-a205-24ac45f09246", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 273.55, + "customerRate": 245.82, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:14.947Z", + "updatedAt": "2021-05-30T16:02:56.406Z", + "payments": [] + }, + { + "id": "2962c8d7-aeab-422e-aa9e-fd76a2c559d6", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 228.98, + "customerRate": 158.21, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:15.005Z", + "updatedAt": "2021-05-30T16:12:13.268Z", + "payments": [] + }, + { + "id": "729c31fb-dcd7-4b1e-bab8-b47f2db27f12", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 45.72, + "customerRate": 131.63, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:14.943Z", + "updatedAt": "2021-05-30T16:04:37.868Z", + "payments": [] + }, + { + "id": "4dff33bc-ef83-425c-a07d-f49a12e2485f", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 200.17, + "customerRate": 286.15, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:14.956Z", + "updatedAt": "2021-05-30T16:05:07.460Z", + "payments": [] + }, + { + "id": "2b2aaaba-2698-4b32-b6b9-e31e040ee023", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 239.68, + "customerRate": 12.51, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:14.958Z", + "updatedAt": "2021-05-30T16:05:31.643Z", + "payments": [] + } + ] + }, + { + "id": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "projectId": 16718, + "userId": "085fc95d-0336-4572-a641-6c8334e7f0c9", + "jobId": "fb2f5f9b-5874-4dcd-af94-727fc0409760", + "status": "sourcing", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 114.33, + "customerRate": 100.25, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-26T08:27:33.886Z", + "updatedAt": "2021-05-30T11:49:22.899Z", + "workPeriods": [ + { + "id": "ddcfc959-d749-45dc-9e9f-f18a893f9e1a", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 216.18, + "customerRate": 269.78, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:23.644Z", + "updatedAt": "2021-05-30T16:12:17.859Z", + "payments": [] + }, + { + "id": "a322ee7e-7f23-4f9f-b2d8-286b574efd7f", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 115.09, + "customerRate": 107.07, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:23.681Z", + "updatedAt": "2021-05-30T16:04:07.452Z", + "payments": [] + }, + { + "id": "77f9b42c-6e67-4363-8b43-aa0b70a904e1", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 295.92, + "customerRate": 38.47, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:23.734Z", + "updatedAt": "2021-05-30T16:04:29.823Z", + "payments": [] + }, + { + "id": "20fc029b-108a-4f12-aec9-ba36619d4ce7", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 124.66, + "customerRate": 105, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:23.730Z", + "updatedAt": "2021-05-30T16:12:17.860Z", + "payments": [] + }, + { + "id": "662f11e5-c02e-460d-989e-1396ff4f00a6", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 4, + "memberRate": 85.69, + "customerRate": 289.93, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:23.678Z", + "updatedAt": "2021-05-30T16:04:45.827Z", + "payments": [] + }, + { + "id": "f8d56b84-b374-4975-81f8-7fab96463243", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": null, + "memberRate": 230.93, + "customerRate": 99.32, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:23.666Z", + "updatedAt": "2021-05-30T16:02:49.121Z", + "payments": [] + } + ] + }, + { + "id": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "projectId": 16870, + "userId": "46550d28-0f34-4292-908f-02f1a34ac278", + "jobId": "fe539bef-9119-4a8c-b7b0-915e7e3a3ba3", + "status": "placed", + "startDate": "2021-01-13", + "endDate": "2021-02-13", + "memberRate": 85.22, + "customerRate": 265.1, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T08:45:58.009Z", + "updatedAt": "2021-05-30T11:49:12.395Z", + "workPeriods": [ + { + "id": "dae10e27-1bec-4004-adb9-25a09a29f58d", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 3, + "memberRate": 82.71, + "customerRate": 103.9, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:13.147Z", + "updatedAt": "2021-05-30T16:12:12.417Z", + "payments": [] + }, + { + "id": "60cfd6f3-4eed-4e2f-98be-f1377648d700", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 5, + "memberRate": 30.12, + "customerRate": 229.65, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:13.155Z", + "updatedAt": "2021-05-30T16:04:52.604Z", + "payments": [] + }, + { + "id": "5356e3d0-fa3e-4a4a-a94b-3d58745c09f7", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 191.92, + "customerRate": 271.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:13.213Z", + "updatedAt": "2021-05-30T16:05:01.471Z", + "payments": [] + }, + { + "id": "394196b1-7fde-4b3e-a6f2-1d95cd93c27d", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 257.15, + "customerRate": 31.9, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:13.168Z", + "updatedAt": "2021-05-30T16:05:21.118Z", + "payments": [] + }, + { + "id": "a6fa8266-f335-4148-96a7-3f63dc66aec4", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 4, + "memberRate": 159.75, + "customerRate": 168.78, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:13.142Z", + "updatedAt": "2021-05-30T16:04:03.881Z", + "payments": [] + } + ] + }, + { + "id": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "projectId": 17103, + "userId": "9e4b1242-9b14-4159-bd0b-de7fa1803ca9", + "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "status": "cancelled", + "startDate": "2021-01-21", + "endDate": "2021-02-21", + "memberRate": 61.33, + "customerRate": 196.21, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T10:25:50.725Z", + "updatedAt": "2021-05-30T11:49:19.410Z", + "workPeriods": [ + { + "id": "217f124d-37db-49a8-9cac-187c5c8b2905", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 157.6, + "customerRate": 211.33, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:20.202Z", + "updatedAt": "2021-05-30T16:05:38.038Z", + "payments": [] + }, + { + "id": "b826c9b8-12f5-4567-bd62-df524bb690a2", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 26.55, + "customerRate": 24.64, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:20.194Z", + "updatedAt": "2021-05-30T16:03:47.803Z", + "payments": [] + }, + { + "id": "3e6436c6-f6d4-4b33-8027-1269b167554f", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 174.63, + "customerRate": 234.94, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:20.200Z", + "updatedAt": "2021-05-30T16:05:18.330Z", + "payments": [] + }, + { + "id": "56f49073-e651-479c-967c-8ba58e36b8e6", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 2, + "memberRate": 166.58, + "customerRate": 107.72, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:20.236Z", + "updatedAt": "2021-05-30T16:12:15.994Z", + "payments": [] + }, + { + "id": "f04d5bb8-abba-4447-92f0-005e823238f8", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-02-21", + "endDate": "2021-02-27", + "daysWorked": 0, + "memberRate": 143.95, + "customerRate": 249.53, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:20.193Z", + "updatedAt": "2021-05-30T16:12:15.996Z", + "payments": [] + }, + { + "id": "fe874682-2ba6-4f42-929b-efc9e05adafd", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-02-14", + "endDate": "2021-02-20", + "daysWorked": 5, + "memberRate": 186.09, + "customerRate": 217.18, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:20.197Z", + "updatedAt": "2021-05-30T15:58:40.816Z", + "payments": [] + } + ] + }, + { + "id": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "projectId": 17324, + "userId": "4709473d-f060-4102-87f8-4d51ff0b34c1", + "jobId": "fefd2618-9b66-4431-9874-1d02d7a37d90", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 271.93, + "customerRate": 188.33, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-20T06:52:40.679Z", + "updatedAt": "2021-05-30T11:49:15.876Z", + "workPeriods": [ + { + "id": "f28bd617-dce3-47c0-a9ab-6b2ff321d206", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 185.99, + "customerRate": 213.97, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:16.700Z", + "updatedAt": "2021-05-30T16:12:14.202Z", + "payments": [] + }, + { + "id": "c447a850-2549-4c6a-ad3e-47cb6b26ac0b", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 22.66, + "customerRate": 215.7, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:16.706Z", + "updatedAt": "2021-05-30T16:03:37.121Z", + "payments": [] + }, + { + "id": "66732a8f-7bab-4e46-8eda-c58f28344114", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 82.71, + "customerRate": 24.46, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:16.759Z", + "updatedAt": "2021-05-30T16:04:44.983Z", + "payments": [] + }, + { + "id": "444fbe9a-616e-443a-a1b5-aadfe7c617ff", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 230.09, + "customerRate": 248.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:16.687Z", + "updatedAt": "2021-05-30T16:05:13.852Z", + "payments": [] + }, + { + "id": "50865971-2fd2-4576-a799-8ab438e9dd75", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 4, + "memberRate": 10.25, + "customerRate": 159.65, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:16.701Z", + "updatedAt": "2021-05-30T16:05:05.541Z", + "payments": [] + } + ] + }, + { + "id": "1ad758ab-c19f-4247-954a-4581420aba8a", + "projectId": 17363, + "userId": "dbf68f12-69a4-4592-a0ab-cf68d9ed7ae4", + "jobId": "fd48d96e-b0f2-43b7-8a48-f4fa194d6bc8", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 114.33, + "customerRate": 217.99, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-27T11:25:35.292Z", + "updatedAt": "2021-05-30T11:49:08.019Z", + "workPeriods": [ + { + "id": "c4c8588e-68a4-4b82-be91-a3d98661ffba", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 54.99, + "customerRate": 109.55, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:08.879Z", + "updatedAt": "2021-05-30T16:03:35.487Z", + "payments": [] + }, + { + "id": "83cb4174-6ee3-4557-97c1-120c46054af6", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": null, + "memberRate": 213.59, + "customerRate": 176.11, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:08.882Z", + "updatedAt": "2021-05-30T16:04:22.560Z", + "payments": [] + }, + { + "id": "8ff2339f-d90a-4ac2-9798-3158d0746d53", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 298.88, + "customerRate": 235.7, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:08.875Z", + "updatedAt": "2021-05-30T16:12:09.701Z", + "payments": [] + }, + { + "id": "5ecaa40c-1fb3-4df7-9870-6fc3c2bc1bca", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 114.59, + "customerRate": 203.92, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:08.877Z", + "updatedAt": "2021-05-30T16:04:53.463Z", + "payments": [] + }, + { + "id": "38afaa09-32da-4d81-b2f5-0c5e31af617f", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 176.02, + "customerRate": 47.87, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:08.867Z", + "updatedAt": "2021-05-30T16:05:21.960Z", + "payments": [] + } + ] + }, + { + "id": "198fb1a7-f662-4e35-aa8b-7dd171d2f519", + "projectId": 17300, + "userId": "2bba34d5-20e4-46d6-bfc1-05736b17afbb", + "jobId": "fd13ad99-f16a-4362-9274-80f5f38895c3", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 104.85, + "customerRate": 138.32, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-20T06:52:10.333Z", + "updatedAt": "2021-05-30T11:49:09.704Z", + "workPeriods": [ + { + "id": "cdda5ed7-5ddf-4985-8856-9b691c196db3", + "resourceBookingId": "198fb1a7-f662-4e35-aa8b-7dd171d2f519", + "userHandle": "GunaK-TopCoder", + "projectId": 17300, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 204.06, + "customerRate": 96.56, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:10.560Z", + "updatedAt": "2021-05-30T16:12:10.667Z", + "payments": [] + }, + { + "id": "ee556bae-58ad-4f64-a48a-ce87362bad3d", + "resourceBookingId": "198fb1a7-f662-4e35-aa8b-7dd171d2f519", + "userHandle": "GunaK-TopCoder", + "projectId": 17300, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 46.62, + "customerRate": 280.07, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:10.570Z", + "updatedAt": "2021-05-30T16:12:10.668Z", + "payments": [] + }, + { + "id": "52c34f5f-290c-4ff0-9d7a-30f43868f83d", + "resourceBookingId": "198fb1a7-f662-4e35-aa8b-7dd171d2f519", + "userHandle": "GunaK-TopCoder", + "projectId": 17300, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 108.35, + "customerRate": 298.27, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:10.568Z", + "updatedAt": "2021-05-30T16:05:03.441Z", + "payments": [] + }, + { + "id": "bd27f1a4-b7ee-4526-a21c-fd8c4955fe5e", + "resourceBookingId": "198fb1a7-f662-4e35-aa8b-7dd171d2f519", + "userHandle": "GunaK-TopCoder", + "projectId": 17300, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 18.57, + "customerRate": 272.37, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:10.566Z", + "updatedAt": "2021-05-30T16:03:43.365Z", + "payments": [] + }, + { + "id": "64c0b0b8-9c77-4e7c-9d0a-14541e8e0a34", + "resourceBookingId": "198fb1a7-f662-4e35-aa8b-7dd171d2f519", + "userHandle": "GunaK-TopCoder", + "projectId": 17300, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 266.82, + "customerRate": 268.61, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:10.622Z", + "updatedAt": "2021-05-30T16:04:49.270Z", + "payments": [] + }, + { + "id": "f41169e6-e9e0-44ce-a54d-5a84a32085c6", + "resourceBookingId": "198fb1a7-f662-4e35-aa8b-7dd171d2f519", + "userHandle": "GunaK-TopCoder", + "projectId": 17300, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 296.93, + "customerRate": 255.03, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:10.558Z", + "updatedAt": "2021-05-30T16:02:52.797Z", + "payments": [] + } + ] + }, + { + "id": "07f73049-e51a-4394-b61f-b75418afa908", + "projectId": 16739, + "userId": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "jobId": "fc5ba131-566f-46fe-8501-79c593241896", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 66, + "customerRate": 114.05, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-12T10:59:20.006Z", + "updatedAt": "2021-05-30T11:49:21.188Z", + "workPeriods": [ + { + "id": "d99a524f-c8a4-4d46-a42c-dbcddd65b6db", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 214.14, + "customerRate": 212.49, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:22.017Z", + "updatedAt": "2021-05-30T16:03:19.706Z", + "payments": [] + }, + { + "id": "5c4bb82b-e617-4c91-861f-5e0825d43c53", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 70.84, + "customerRate": 136.39, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:21.977Z", + "updatedAt": "2021-05-30T16:04:55.294Z", + "payments": [] + }, + { + "id": "7ff4804e-2a65-4f8b-af5b-24b58c066fd4", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 25.99, + "customerRate": 122.72, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:22.068Z", + "updatedAt": "2021-05-30T16:12:16.993Z", + "payments": [] + }, + { + "id": "da043aba-161e-4894-a3d5-d63678ac89b0", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": null, + "memberRate": 71.99, + "customerRate": 155.48, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:21.974Z", + "updatedAt": "2021-05-30T16:03:18.831Z", + "payments": [] + }, + { + "id": "410e034d-ee48-4a18-aa65-679ef7efcb80", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 4, + "memberRate": 146.28, + "customerRate": 21.33, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:21.961Z", + "updatedAt": "2021-05-30T16:05:16.466Z", + "payments": [] + } + ] + }, + { + "id": "d3cd14c8-9ae8-446a-b554-69240c93a20e", + "projectId": 17091, + "userId": "3e654566-0d6b-404a-a000-c8640252c0e1", + "jobId": "fb8b92f6-4ffb-4ba6-8c38-c2d4a151f76b", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 201.77, + "customerRate": 296.66, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:58:18.513Z", + "updatedAt": "2021-05-30T11:48:25.595Z", + "workPeriods": [] + }, + { + "id": "7d81f99f-e4d2-49ff-8ce5-39832ea972fe", + "projectId": 16870, + "userId": "74219092-52ee-4434-a35e-25f000369645", + "jobId": "fe8da845-5313-496f-b859-9824bd06a0db", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 211.1, + "customerRate": 100.25, + "rateType": "monthly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:30:42.397Z", + "updatedAt": "2021-05-30T11:48:48.669Z", + "workPeriods": [] + }, + { + "id": "e7d96c52-a7ec-40e1-be64-6ef21fab4a1e", + "projectId": 16718, + "userId": "d82d4d41-1f25-4faf-ac24-3b5f8a138fac", + "jobId": "fb2f5f9b-5874-4dcd-af94-727fc0409760", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 271.93, + "customerRate": 58.22, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-17T13:44:23.792Z", + "updatedAt": "2021-05-30T11:48:19.456Z", + "workPeriods": [] + }, + { + "id": "5c3536b3-7523-4706-9396-eaa44ec608bb", + "projectId": 17103, + "userId": "93814e61-3b44-40bf-acaf-477857f52f90", + "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 115.29, + "customerRate": 114.05, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:53:01.882Z", + "updatedAt": "2021-05-30T11:48:58.248Z", + "workPeriods": [] + }, + { + "id": "1553e801-61c1-4069-ac58-bcb206ac2e44", + "projectId": 16781, + "userId": "bea5b4c1-922d-4800-ae27-c2f45b7e20bc", + "jobId": "ff3feeae-d4f7-457c-bff7-215be5efe2b8", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 186.53, + "customerRate": 265.1, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-19T10:07:50.071Z", + "updatedAt": "2021-05-30T11:49:11.520Z", + "workPeriods": [] + }, + { + "id": "a386f79d-724e-4c13-bf85-b8e5a0394617", + "projectId": 16706, + "userId": "f21455ae-a5f1-41a7-86eb-152e0e113b6e", + "jobId": "fc2b006d-997b-49c3-a414-59ee54a48f9f", + "status": "sourcing", + "startDate": null, + "endDate": null, + "memberRate": 115.29, + "customerRate": 146.2, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-26T08:25:01.121Z", + "updatedAt": "2021-05-30T11:39:24.762Z", + "workPeriods": [] + }, + { + "id": "4141d57c-2712-4dab-8140-904ab4364e98", + "projectId": 16714, + "userId": "fc04aa5d-9c34-4dd6-be8d-10ec0e3e6d01", + "jobId": "fc0240f0-8c8f-40ce-a551-e83b45673098", + "status": "sourcing", + "startDate": null, + "endDate": null, + "memberRate": 60.63, + "customerRate": 196.21, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-25T13:12:14.707Z", + "updatedAt": "2021-05-30T11:49:03.637Z", + "workPeriods": [] + }, + { + "id": "042d8158-3cee-4289-839e-1f2a73af1859", + "projectId": 16805, + "userId": "388f9618-5a2c-4a58-b587-3b3dfbb3f584", + "jobId": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 115.29, + "customerRate": 111.21, + "rateType": "monthly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-04-20T08:04:17.892Z", + "updatedAt": "2021-05-30T11:49:24.627Z", + "workPeriods": [] + }, + { + "id": "21ae6f7f-f594-496a-9d87-175fd5820286", + "projectId": 16870, + "userId": "2bba34d5-20e4-46d6-bfc1-05736b17afbb", + "jobId": "fed687e1-4257-48bb-806c-38712f9bf14f", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 30.29, + "customerRate": 217.99, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-04-20T08:04:14.718Z", + "updatedAt": "2021-05-30T11:49:05.373Z", + "workPeriods": [] + }, + { + "id": "41fb1035-3165-4ff9-a2df-62c700fb8b37", + "projectId": 17290, + "userId": "b5bc4d91-2396-467b-8f7d-9a56ffb0feb0", + "jobId": "fe600350-0a6d-4dac-922f-a6a7d285daa1", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 240.84, + "customerRate": 146.2, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-18T04:01:38.141Z", + "updatedAt": "2021-05-30T11:49:02.754Z", + "workPeriods": [] + }, + { + "id": "f0738f2a-c837-42e3-acdf-0e1324d77e53", + "projectId": 16739, + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "jobId": "fc5ba131-566f-46fe-8501-79c593241896", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 271.93, + "customerRate": 258.37, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-17T13:43:44.675Z", + "updatedAt": "2021-05-30T11:48:16.870Z", + "workPeriods": [] } ] -} +} \ No newline at end of file diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 74155753..487bf3f9 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", + "_postman_id": "7f931491-50de-42cf-9e15-24d7a6675667", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -8016,6 +8016,2058 @@ } ] }, + { + "name": "Extended Search Scenarios", + "item": [ + { + "name": "search RB sortBy id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=id&sortOrder=asc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "id" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id", + "disabled": true + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=status&sortOrder=asc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "status" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,status", + "disabled": true + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods.id,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods.id,workPeriods.startDate", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods.id,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods.id,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy rateType", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=rateType&sortOrder=asc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "rateType" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType", + "disabled": true + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods.id,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods.id,workPeriods.startDate", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods.id,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods.id,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy startDate", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=10&sortBy=startDate&sortOrder=asc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "10" + }, + { + "key": "sortBy", + "value": "startDate" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate", + "disabled": true + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods.id,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods.id,workPeriods.startDate", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods.id,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods.id,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy endDate", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=10&sortBy=endDate&sortOrder=asc&fields=id,endDate,workPeriods", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "10" + }, + { + "key": "sortBy", + "value": "endDate" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate", + "disabled": true + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods.id,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods.id,workPeriods.startDate", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods.id,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods.id,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods" + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy customerRate", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=customerRate&sortOrder=asc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "customerRate" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate", + "disabled": true + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods.id,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods.id,workPeriods.startDate", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods.id,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods.id,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy memberRate", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=memberRate&sortOrder=asc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "memberRate" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate", + "disabled": true + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods.id,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods.id,workPeriods.startDate", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods.id,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods.id,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy workPeriods.userHandle", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=10&sortBy=workPeriods.userHandle&workPeriods.startDate=2021-01-03&sortOrder=asc&fields=id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "10" + }, + { + "key": "sortBy", + "value": "workPeriods.userHandle" + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle" + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy workPeriods.daysWorked", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=10&sortBy=workPeriods.daysWorked&workPeriods.startDate=2021-01-10&sortOrder=asc&fields=id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "10" + }, + { + "key": "sortBy", + "value": "workPeriods.daysWorked" + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-10" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked" + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-16", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy workPeriods.customerRate", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=workPeriods.customerRate&workPeriods.startDate=2021-01-03&sortOrder=asc&fields=id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "workPeriods.customerRate" + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate" + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy workPeriods.memberRate", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=workPeriods.memberRate&workPeriods.startDate=2021-01-03&sortOrder=asc&fields=id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "workPeriods.memberRate" + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate" + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy workPeriods.paymentStatus", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=workPeriods.paymentStatus&workPeriods.startDate=2021-01-03&sortOrder=asc&fields=id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "workPeriods.paymentStatus" + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus" + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + } + ] + }, { "name": "create resource booking with booking manager", "event": [ @@ -27007,4 +29059,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/topcoder-bookings.postman_environment.json b/docs/topcoder-bookings.postman_environment.json index 837b55db..12611873 100644 --- a/docs/topcoder-bookings.postman_environment.json +++ b/docs/topcoder-bookings.postman_environment.json @@ -1,5 +1,5 @@ { - "id": "228f4dcc-6914-462e-9b56-3285b643a2f8", + "id": "d4a3eeb9-b0f8-4dca-ae1a-78bc6e43bcce", "name": "topcoder-bookings", "values": [ { @@ -142,51 +142,6 @@ "value": "17234", "enabled": true }, - { - "key": "jobId", - "value": "", - "enabled": true - }, - { - "key": "jobIdCreatedByM2M", - "value": "", - "enabled": true - }, - { - "key": "jobIdCreatedByMember", - "value": "", - "enabled": true - }, - { - "key": "jobCandidateId", - "value": "", - "enabled": true - }, - { - "key": "jobCandidateIdCreatedByM2M", - "value": "", - "enabled": true - }, - { - "key": "resourceBookingId", - "value": "", - "enabled": true - }, - { - "key": "resourceBookingIdCreatedByM2M", - "value": "", - "enabled": true - }, - { - "key": "workPeriodId", - "value": "", - "enabled": true - }, - { - "key": "workPeriodIdCreatedByM2M", - "value": "", - "enabled": true - }, { "key": "token_m2m_create_work_period", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6dGFhcy13b3JrUGVyaW9kcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.tgKxTrlI8bu6CVFk4-kFB1gKHL-L6X8akKYYREjZiSE", @@ -242,106 +197,6 @@ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJhbGw6dGFhcy13b3JrUGVyaW9kUGF5bWVudHMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.ut5vdW-124nBshaeGBvg4mCue4XlRljWlV7OneJk4i4", "enabled": true }, - { - "key": "job_id_created_by_administrator", - "value": "", - "enabled": true - }, - { - "key": "resource_bookings_id_created_by_administrator", - "value": "", - "enabled": true - }, - { - "key": "workPeriodId_created_by_administrator", - "value": "", - "enabled": true - }, - { - "key": "job_id_created_by_member", - "value": "", - "enabled": true - }, - { - "key": "resource_booking_id_created_by_member", - "value": "", - "enabled": true - }, - { - "key": "resource_booking_id_created_for_member", - "value": "", - "enabled": true - }, - { - "key": "workPeriodId_created_for_member", - "value": "", - "enabled": true - }, - { - "key": "resource_booking_id_created_for_connect_manager", - "value": "", - "enabled": true - }, - { - "key": "job_id_created_by_connect_manager", - "value": "", - "enabled": true - }, - { - "key": "workPeriodId_created_for_connect_manager", - "value": "", - "enabled": true - }, - { - "key": "job_candidate_id_created_by_administrator", - "value": "", - "enabled": true - }, - { - "key": "workPeriodPaymentId", - "value": "", - "enabled": true - }, - { - "key": "workPeriodPaymentIdCreatedByM2M", - "value": "", - "enabled": true - }, - { - "key": "workPeriodPaymentId_created_by_administrator", - "value": "", - "enabled": true - }, - { - "key": "job_id_created_for_member", - "value": "", - "enabled": true - }, - { - "key": "resource_bookings_id_created_for_member", - "value": "", - "enabled": true - }, - { - "key": "workPeriodPaymentId_created_for_member", - "value": "", - "enabled": true - }, - { - "key": "job_id_created_for_connect_manager", - "value": "", - "enabled": true - }, - { - "key": "resource_bookings_id_created_for_connect_manager", - "value": "", - "enabled": true - }, - { - "key": "workPeriodPaymentId_created_for_connect_manager", - "value": "", - "enabled": true - }, { "key": "interviewRound", "value": "1", @@ -352,46 +207,16 @@ "value": "1", "enabled": true }, - { - "key": "job_id_created_for_member", - "value": "", - "enabled": true - }, - { - "key": "job_candidate_id_created_for_member", - "value": "", - "enabled": true - }, { "key": "interview_round_created_for_member", "value": "1", "enabled": true }, - { - "key": "job_id_created_for_connect_manager", - "value": "", - "enabled": true - }, - { - "key": "job_candidate_id_created_for_connect_manager", - "value": "", - "enabled": true - }, { "key": "interview_round_created_for_connect_manager", "value": "1", "enabled": true }, - { - "key": "completedInterviewJobCandidateId", - "value": "", - "enabled": true - }, - { - "key": "completedInterviewRound", - "value": "", - "enabled": true - }, { "key": "token_m2m_create_interviews", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6dGFhcy1pbnRlcnZpZXdzIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.VD5j8qdgK3ZPctqD-Nb5KKfSeFIuyajc7Q-wQ_kabIk", @@ -421,49 +246,9 @@ "key": "token_m2m_read_jobCandidates_all_interviews", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJyZWFkOnRhYXMtam9iQ2FuZGlkYXRlcyBhbGw6dGFhcy1pbnRlcnZpZXdzIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.XQj74JSHp98XKxa1eZnMMpHxGpHeZAHVhLvFAN7gHBY", "enabled": true - }, - { - "key": "jobCandidateId_2", - "value": "", - "enabled": true - }, - { - "key": "jobCandidateId_3", - "value": "", - "enabled": true - }, - { - "key": "jobCandidateId_4", - "value": "", - "enabled": true - }, - { - "key": "jobCandidateId_5", - "value": "", - "enabled": true - }, - { - "key": "interviewId", - "value": "", - "enabled": true - }, - { - "key": "interview_id_created_by_administrator", - "value": "", - "enabled": true - }, - { - "key": "interview_id_created_for_member", - "value": "", - "enabled": true - }, - { - "key": "interview_id_created_for_connect_manager", - "value": "", - "enabled": true } ], "_postman_variable_scope": "environment", - "_postman_exported_at": "2021-05-10T05:06:38.661Z", - "_postman_exported_using": "Postman/8.3.1" + "_postman_exported_at": "2021-05-30T20:07:50.354Z", + "_postman_exported_using": "Postman/8.5.1" } \ No newline at end of file diff --git a/src/common/helper.js b/src/common/helper.js index e68e5c58..afa9d89f 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -2,50 +2,50 @@ * This file defines helper methods */ -const fs = require('fs'); -const querystring = require('querystring'); -const Confirm = require('prompt-confirm'); -const Bottleneck = require('bottleneck'); -const AWS = require('aws-sdk'); -const config = require('config'); -const HttpStatus = require('http-status-codes'); -const _ = require('lodash'); -const request = require('superagent'); -const elasticsearch = require('@elastic/elasticsearch'); +const fs = require('fs') +const querystring = require('querystring') +const Confirm = require('prompt-confirm') +const Bottleneck = require('bottleneck') +const AWS = require('aws-sdk') +const config = require('config') +const HttpStatus = require('http-status-codes') +const _ = require('lodash') +const request = require('superagent') +const elasticsearch = require('@elastic/elasticsearch') const { - ResponseError: ESResponseError, -} = require('@elastic/elasticsearch/lib/errors'); -const errors = require('../common/errors'); -const logger = require('./logger'); -const models = require('../models'); -const eventDispatcher = require('./eventDispatcher'); -const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper'); -const moment = require('moment'); + ResponseError: ESResponseError +} = require('@elastic/elasticsearch/lib/errors') +const errors = require('../common/errors') +const logger = require('./logger') +const models = require('../models') +const eventDispatcher = require('./eventDispatcher') +const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') +const moment = require('moment') const localLogger = { debug: (message) => logger.debug({ component: 'helper', context: message.context, - message: message.message, + message: message.message }), error: (message) => logger.error({ component: 'helper', context: message.context, - message: message.message, + message: message.message }), info: (message) => logger.info({ component: 'helper', context: message.context, - message: message.message, - }), -}; + message: message.message + }) +} -AWS.config.region = config.esConfig.AWS_REGION; +AWS.config.region = config.esConfig.AWS_REGION -const m2mAuth = require('tc-core-library-js').auth.m2m; +const m2mAuth = require('tc-core-library-js').auth.m2m const m2m = m2mAuth( _.pick(config, [ @@ -53,9 +53,9 @@ const m2m = m2mAuth( 'AUTH0_AUDIENCE', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', - 'AUTH0_PROXY_SERVER_URL', + 'AUTH0_PROXY_SERVER_URL' ]) -); +) const m2mForUbahn = m2mAuth({ AUTH0_AUDIENCE: config.AUTH0_AUDIENCE_UBAHN, @@ -64,20 +64,20 @@ const m2mForUbahn = m2mAuth({ 'TOKEN_CACHE_TIME', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', - 'AUTH0_PROXY_SERVER_URL', - ]), -}); + 'AUTH0_PROXY_SERVER_URL' + ]) +}) -let busApiClient; +let busApiClient /** * Get bus api client. * * @returns {Object} the bus api client */ -function getBusApiClient() { +function getBusApiClient () { if (busApiClient) { - return busApiClient; + return busApiClient } busApiClient = busApi( _.pick(config, [ @@ -88,17 +88,17 @@ function getBusApiClient() { 'AUTH0_CLIENT_SECRET', 'BUSAPI_URL', 'KAFKA_ERROR_TOPIC', - 'AUTH0_PROXY_SERVER_URL', + 'AUTH0_PROXY_SERVER_URL' ]) - ); - return busApiClient; + ) + return busApiClient } // ES Client mapping -const esClients = {}; +const esClients = {} // The es index property mapping -const esIndexPropertyMapping = {}; +const esIndexPropertyMapping = {} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { projectId: { type: 'integer' }, externalId: { type: 'keyword' }, @@ -116,8 +116,8 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { jobId: { type: 'keyword' }, userId: { type: 'keyword' }, @@ -150,14 +150,14 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, updatedBy: { type: 'keyword' }, - deletedAt: { type: 'date' }, - }, + deletedAt: { type: 'date' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { projectId: { type: 'integer' }, userId: { type: 'keyword' }, @@ -174,7 +174,10 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { properties: { id: { type: 'keyword' }, resourceBookingId: { type: 'keyword' }, - userHandle: { type: 'keyword' }, + userHandle: { + type: 'keyword', + normalizer: 'lowercaseNormalizer' + }, projectId: { type: 'integer' }, userId: { type: 'keyword' }, startDate: { type: 'date', format: 'yyyy-MM-dd' }, @@ -195,32 +198,32 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, - }, + updatedBy: { type: 'keyword' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, - }, + updatedBy: { type: 'keyword' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} /** * Get the first parameter from cli arguments */ -function getParamFromCliArgs() { - const filteredArgs = process.argv.filter((arg) => !arg.includes('--')); +function getParamFromCliArgs () { + const filteredArgs = process.argv.filter((arg) => !arg.includes('--')) if (filteredArgs.length > 2) { - return filteredArgs[2]; + return filteredArgs[2] } - return null; + return null } /** @@ -228,18 +231,18 @@ function getParamFromCliArgs() { * @param {string} promptQuery the query to ask the user * @param {function} cb the callback function */ -async function promptUser(promptQuery, cb) { +async function promptUser (promptQuery, cb) { if (process.argv.includes('--force')) { - await cb(); - return; + await cb() + return } - const prompt = new Confirm(promptQuery); + const prompt = new Confirm(promptQuery) prompt.ask(async (answer) => { if (answer) { - await cb(); + await cb() } - }); + }) } /** @@ -248,23 +251,38 @@ async function promptUser(promptQuery, cb) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function createIndex(index, logger, esClient = null) { +async function createIndex (index, logger, esClient = null) { if (!esClient) { - esClient = getESClient(); + esClient = getESClient() } - await esClient.indices.create({ + await esClient.indices.create({ index }) + await esClient.indices.close({ index }) + await esClient.indices.putSettings({ + index: index, + body: { + settings: { + analysis: { + normalizer: { + lowercaseNormalizer: { + filter: ['lowercase'] + } + } + } + } + } + }) + await esClient.indices.open({ index }) + await esClient.indices.putMapping({ index, body: { - mappings: { - properties: esIndexPropertyMapping[index], - }, - }, - }); + properties: esIndexPropertyMapping[index] + } + }) logger.info({ component: 'createIndex', - message: `ES Index ${index} creation succeeded!`, - }); + message: `ES Index ${index} creation succeeded!` + }) } /** @@ -273,45 +291,45 @@ async function createIndex(index, logger, esClient = null) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function deleteIndex(index, logger, esClient = null) { +async function deleteIndex (index, logger, esClient = null) { if (!esClient) { - esClient = getESClient(); + esClient = getESClient() } - await esClient.indices.delete({ index }); + await esClient.indices.delete({ index }) logger.info({ component: 'deleteIndex', - message: `ES Index ${index} deletion succeeded!`, - }); + message: `ES Index ${index} deletion succeeded!` + }) } /** * Split data into bulks * @param {Array} data the array of data to split */ -function getBulksFromDocuments(data) { - const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6; - const bulks = []; - let documentIndex = 0; - let currentBulkSize = 0; - let currentBulk = []; +function getBulksFromDocuments (data) { + const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6 + const bulks = [] + let documentIndex = 0 + let currentBulkSize = 0 + let currentBulk = [] while (true) { // break loop when parsed all documents if (documentIndex >= data.length) { - bulks.push(currentBulk); - break; + bulks.push(currentBulk) + break } // check if current document size is greater than the max bulk size, if so, throw error const currentDocumentSize = Buffer.byteLength( JSON.stringify(data[documentIndex]), 'utf-8' - ); + ) if (maxBytes < currentDocumentSize) { throw new Error( `Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.` - ); + ) } if ( @@ -320,17 +338,17 @@ function getBulksFromDocuments(data) { ) { // if adding the current document goes over the max bulk size OR goes over max number of docs // then push the current bulk to bulks array and reset the current bulk - bulks.push(currentBulk); - currentBulk = []; - currentBulkSize = 0; + bulks.push(currentBulk) + currentBulk = [] + currentBulkSize = 0 } else { // otherwise, add document to current bulk - currentBulk.push(data[documentIndex]); - currentBulkSize += currentDocumentSize; - documentIndex++; + currentBulk.push(data[documentIndex]) + currentBulkSize += currentDocumentSize + documentIndex++ } } - return bulks; + return bulks } /** @@ -339,57 +357,57 @@ function getBulksFromDocuments(data) { * @param {Object} indexName the index name * @param {Object} logger the logger object */ -async function indexBulkDataToES(modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); +async function indexBulkDataToES (modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) logger.info({ component: 'indexBulkDataToES', - message: `Reindexing of ${modelName}s started!`, - }); + message: `Reindexing of ${modelName}s started!` + }) - const esClient = getESClient(); + const esClient = getESClient() // clear index - const indexExistsRes = await esClient.indices.exists({ index: indexName }); + const indexExistsRes = await esClient.indices.exists({ index: indexName }) if (indexExistsRes.statusCode !== 404) { - await deleteIndex(indexName, logger, esClient); + await deleteIndex(indexName, logger, esClient) } - await createIndex(indexName, logger, esClient); + await createIndex(indexName, logger, esClient) // get data from db logger.info({ component: 'indexBulkDataToES', - message: 'Getting data from database', - }); - const model = models[modelName]; - const data = await model.findAll({ include }); - const rawObjects = _.map(data, (r) => r.toJSON()); + message: 'Getting data from database' + }) + const model = models[modelName] + const data = await model.findAll({ include }) + const rawObjects = _.map(data, (r) => r.toJSON()) if (_.isEmpty(rawObjects)) { logger.info({ component: 'indexBulkDataToES', - message: `No data in database for ${modelName}`, - }); - return; + message: `No data in database for ${modelName}` + }) + return } - const bulks = getBulksFromDocuments(rawObjects); + const bulks = getBulksFromDocuments(rawObjects) - const startTime = Date.now(); - let doneCount = 0; + const startTime = Date.now() + let doneCount = 0 for (const bulk of bulks) { // send bulk to esclient const body = bulk.flatMap((doc) => [ { index: { _index: indexName, _id: doc.id } }, - doc, - ]); - await esClient.bulk({ refresh: true, body }); - doneCount += bulk.length; + doc + ]) + await esClient.bulk({ refresh: true, body }) + doneCount += bulk.length // log metrics - const timeSpent = Date.now() - startTime; - const avgTimePerDocument = timeSpent / doneCount; - const estimatedLength = avgTimePerDocument * data.length; - const timeLeft = startTime + estimatedLength - Date.now(); + const timeSpent = Date.now() - startTime + const avgTimePerDocument = timeSpent / doneCount + const estimatedLength = avgTimePerDocument * data.length + const timeLeft = startTime + estimatedLength - Date.now() logger.info({ component: 'indexBulkDataToES', message: `Processed ${doneCount} of ${ @@ -398,8 +416,8 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { avgTimePerDocument )}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime( timeLeft - )}`, - }); + )}` + }) } } @@ -410,36 +428,36 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { * @param {string} id the job id * @param {Object} logger the logger object */ -async function indexDataToEsById(id, modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); +async function indexDataToEsById (id, modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) logger.info({ component: 'indexDataToEsById', - message: `Reindexing of ${modelName} with id ${id} started!`, - }); - const esClient = getESClient(); + message: `Reindexing of ${modelName} with id ${id} started!` + }) + const esClient = getESClient() logger.info({ component: 'indexDataToEsById', - message: 'Getting data from database', - }); - const model = models[modelName]; + message: 'Getting data from database' + }) + const model = models[modelName] - const data = await model.findById(id, include); + const data = await model.findById(id, include) logger.info({ component: 'indexDataToEsById', - message: 'Indexing data into Elasticsearch', - }); + message: 'Indexing data into Elasticsearch' + }) await esClient.index({ index: indexName, id: id, - body: data.dataValues, - }); + body: data.dataValues + }) logger.info({ component: 'indexDataToEsById', - message: 'Indexing complete!', - }); + message: 'Indexing complete!' + }) } /** @@ -448,68 +466,68 @@ async function indexDataToEsById(id, modelOpts, indexName, logger) { * @param {Array} dataModels the data models to import * @param {Object} logger the logger object */ -async function importData(pathToFile, dataModels, logger) { +async function importData (pathToFile, dataModels, logger) { // check if file exists if (!fs.existsSync(pathToFile)) { - throw new Error(`File with path ${pathToFile} does not exist`); + throw new Error(`File with path ${pathToFile} does not exist`) } // clear database - logger.info({ component: 'importData', message: 'Clearing database...' }); - await models.sequelize.sync({ force: true }); + logger.info({ component: 'importData', message: 'Clearing database...' }) + await models.sequelize.sync({ force: true }) - let transaction = null; - let currentModelName = null; + let transaction = null + let currentModelName = null try { // Start a transaction - transaction = await models.sequelize.transaction(); - const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()); + transaction = await models.sequelize.transaction() + const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()) for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index]; - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); + const modelOpts = dataModels[index] + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) - currentModelName = modelName; - const model = models[modelName]; - const modelRecords = jsonData[modelName]; + currentModelName = modelName + const model = models[modelName] + const modelRecords = jsonData[modelName] if (modelRecords && modelRecords.length > 0) { logger.info({ component: 'importData', - message: `Importing data for model: ${modelName}`, - }); + message: `Importing data for model: ${modelName}` + }) - await model.bulkCreate(modelRecords, { include, transaction }); + await model.bulkCreate(modelRecords, { include, transaction }) logger.info({ component: 'importData', - message: `Records imported for model: ${modelName} = ${modelRecords.length}`, - }); + message: `Records imported for model: ${modelName} = ${modelRecords.length}` + }) } else { logger.info({ component: 'importData', - message: `No records to import for model: ${modelName}`, - }); + message: `No records to import for model: ${modelName}` + }) } } // commit transaction only if all things went ok logger.info({ component: 'importData', - message: 'committing transaction to database...', - }); - await transaction.commit(); + message: 'committing transaction to database...' + }) + await transaction.commit() } catch (error) { logger.error({ component: 'importData', - message: `Error while writing data of model: ${currentModelName}`, - }); + message: `Error while writing data of model: ${currentModelName}` + }) // rollback all insert operations if (transaction) { logger.info({ component: 'importData', - message: 'rollback database transaction...', - }); - transaction.rollback(); + message: 'rollback database transaction...' + }) + transaction.rollback() } if (error.name && error.errors && error.fields) { // For sequelize validation errors, we throw only fields with data that helps in debugging error, @@ -519,11 +537,11 @@ async function importData(pathToFile, dataModels, logger) { modelName: currentModelName, name: error.name, errors: error.errors, - fields: error.fields, + fields: error.fields }) - ); + ) } else { - throw error; + throw error } } @@ -533,10 +551,10 @@ async function importData(pathToFile, dataModels, logger) { include: [ { model: models.Interview, - as: 'interviews', - }, - ], - }; + as: 'interviews' + } + ] + } const resourceBookingModelOpts = { modelName: 'ResourceBooking', include: [ @@ -546,23 +564,23 @@ async function importData(pathToFile, dataModels, logger) { include: [ { model: models.WorkPeriodPayment, - as: 'payments', - }, - ], - }, - ], - }; - await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger); + as: 'payments' + } + ] + } + ] + } + await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) await indexBulkDataToES( jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger - ); + ) await indexBulkDataToES( resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger - ); + ) } /** @@ -571,74 +589,74 @@ async function importData(pathToFile, dataModels, logger) { * @param {Array} dataModels the data models to export * @param {Object} logger the logger object */ -async function exportData(pathToFile, dataModels, logger) { +async function exportData (pathToFile, dataModels, logger) { logger.info({ component: 'exportData', - message: `Start Saving data to file with path ${pathToFile}....`, - }); + message: `Start Saving data to file with path ${pathToFile}....` + }) - const allModelsRecords = {}; + const allModelsRecords = {} for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index]; - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); - const modelRecords = await models[modelName].findAll({ include }); - const rawRecords = _.map(modelRecords, (r) => r.toJSON()); - allModelsRecords[modelName] = rawRecords; + const modelOpts = dataModels[index] + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) + const modelRecords = await models[modelName].findAll({ include }) + const rawRecords = _.map(modelRecords, (r) => r.toJSON()) + allModelsRecords[modelName] = rawRecords logger.info({ component: 'exportData', - message: `Records loaded for model: ${modelName} = ${rawRecords.length}`, - }); + message: `Records loaded for model: ${modelName} = ${rawRecords.length}` + }) } - fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)); + fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)) logger.info({ component: 'exportData', - message: 'End Saving data to file....', - }); + message: 'End Saving data to file....' + }) } /** * Format a time in milliseconds into a human readable format * @param {Date} milliseconds the number of milliseconds */ -function formatTime(millisec) { - const ms = Math.floor(millisec % 1000); - const secs = Math.floor((millisec / 1000) % 60); - const mins = Math.floor((millisec / (1000 * 60)) % 60); - const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24); - const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7); - const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4); - const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12); - const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)); - - let formattedTime = '0 milliseconds'; +function formatTime (millisec) { + const ms = Math.floor(millisec % 1000) + const secs = Math.floor((millisec / 1000) % 60) + const mins = Math.floor((millisec / (1000 * 60)) % 60) + const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24) + const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7) + const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4) + const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12) + const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)) + + let formattedTime = '0 milliseconds' if (ms > 0) { - formattedTime = `${ms} milliseconds`; + formattedTime = `${ms} milliseconds` } if (secs > 0) { - formattedTime = `${secs} seconds ${formattedTime}`; + formattedTime = `${secs} seconds ${formattedTime}` } if (mins > 0) { - formattedTime = `${mins} minutes ${formattedTime}`; + formattedTime = `${mins} minutes ${formattedTime}` } if (hrs > 0) { - formattedTime = `${hrs} hours ${formattedTime}`; + formattedTime = `${hrs} hours ${formattedTime}` } if (days > 0) { - formattedTime = `${days} days ${formattedTime}`; + formattedTime = `${days} days ${formattedTime}` } if (weeks > 0) { - formattedTime = `${weeks} weeks ${formattedTime}`; + formattedTime = `${weeks} weeks ${formattedTime}` } if (mnths > 0) { - formattedTime = `${mnths} months ${formattedTime}`; + formattedTime = `${mnths} months ${formattedTime}` } if (yrs > 0) { - formattedTime = `${yrs} years ${formattedTime}`; + formattedTime = `${yrs} years ${formattedTime}` } - return formattedTime.trim(); + return formattedTime.trim() } /** @@ -647,30 +665,30 @@ function formatTime(millisec) { * @param {Array} source the array in which to search for the term * @param {Array | String} term the term to search */ -function checkIfExists(source, term) { - let terms; +function checkIfExists (source, term) { + let terms if (!_.isArray(source)) { - throw new Error('Source argument should be an array'); + throw new Error('Source argument should be an array') } - source = source.map((s) => s.toLowerCase()); + source = source.map((s) => s.toLowerCase()) if (_.isString(term)) { - terms = term.toLowerCase().split(' '); + terms = term.toLowerCase().split(' ') } else if (_.isArray(term)) { - terms = term.map((t) => t.toLowerCase()); + terms = term.map((t) => t.toLowerCase()) } else { - throw new Error('Term argument should be either a string or an array'); + throw new Error('Term argument should be either a string or an array') } for (let i = 0; i < terms.length; i++) { if (source.includes(terms[i])) { - return true; + return true } } - return false; + return false } /** @@ -678,10 +696,10 @@ function checkIfExists(source, term) { * @param {Function} fn the async function * @returns {Function} the wrapped function */ -function wrapExpress(fn) { +function wrapExpress (fn) { return function (req, res, next) { - fn(req, res, next).catch(next); - }; + fn(req, res, next).catch(next) + } } /** @@ -689,20 +707,20 @@ function wrapExpress(fn) { * @param obj the object (controller exports) * @returns {Object|Array} the wrapped object */ -function autoWrapExpress(obj) { +function autoWrapExpress (obj) { if (_.isArray(obj)) { - return obj.map(autoWrapExpress); + return obj.map(autoWrapExpress) } if (_.isFunction(obj)) { if (obj.constructor.name === 'AsyncFunction') { - return wrapExpress(obj); + return wrapExpress(obj) } - return obj; + return obj } _.each(obj, (value, key) => { - obj[key] = autoWrapExpress(value); - }); - return obj; + obj[key] = autoWrapExpress(value) + }) + return obj } /** @@ -711,11 +729,11 @@ function autoWrapExpress(obj) { * @param {Number} page the page number * @returns {String} link for the page */ -function getPageLink(req, page) { - const q = _.assignIn({}, req.query, { page }); +function getPageLink (req, page) { + const q = _.assignIn({}, req.query, { page }) return `${req.protocol}://${req.get('Host')}${req.baseUrl}${ req.path - }?${querystring.stringify(q)}`; + }?${querystring.stringify(q)}` } /** @@ -724,31 +742,31 @@ function getPageLink(req, page) { * @param {Object} res the HTTP response * @param {Object} result the operation result */ -function setResHeaders(req, res, result) { - const totalPages = Math.ceil(result.total / result.perPage); +function setResHeaders (req, res, result) { + const totalPages = Math.ceil(result.total / result.perPage) if (result.page > 1) { - res.set('X-Prev-Page', result.page - 1); + res.set('X-Prev-Page', result.page - 1) } if (result.page < totalPages) { - res.set('X-Next-Page', result.page + 1); + res.set('X-Next-Page', result.page + 1) } - res.set('X-Page', result.page); - res.set('X-Per-Page', result.perPage); - res.set('X-Total', result.total); - res.set('X-Total-Pages', totalPages); + res.set('X-Page', result.page) + res.set('X-Per-Page', result.perPage) + res.set('X-Total', result.total) + res.set('X-Total-Pages', totalPages) // set Link header if (totalPages > 0) { let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink( req, totalPages - )}>; rel="last"`; + )}>; rel="last"` if (result.page > 1) { - link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"`; + link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"` } if (result.page < totalPages) { - link += `, <${getPageLink(req, result.page + 1)}>; rel="next"`; + link += `, <${getPageLink(req, result.page + 1)}>; rel="next"` } - res.set('Link', link); + res.set('Link', link) } } @@ -756,30 +774,30 @@ function setResHeaders(req, res, result) { * Get ES Client * @return {Object} Elastic Host Client Instance */ -function getESClient() { +function getESClient () { if (esClients.client) { - return esClients.client; + return esClients.client } - const host = config.esConfig.HOST; - const cloudId = config.esConfig.ELASTICCLOUD.id; + const host = config.esConfig.HOST + const cloudId = config.esConfig.ELASTICCLOUD.id if (cloudId) { // Elastic Cloud configuration esClients.client = new elasticsearch.Client({ cloud: { - id: cloudId, + id: cloudId }, auth: { username: config.esConfig.ELASTICCLOUD.username, - password: config.esConfig.ELASTICCLOUD.password, - }, - }); + password: config.esConfig.ELASTICCLOUD.password + } + }) } else { esClients.client = new elasticsearch.Client({ - node: host, - }); + node: host + }) } - return esClients.client; + return esClients.client } /* @@ -790,8 +808,8 @@ const getM2MToken = async () => { return await m2m.getMachineToken( config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET - ); -}; + ) +} /* * Function to get M2M token for U-Bahn @@ -801,8 +819,8 @@ const getM2MUbahnToken = async () => { return await m2mForUbahn.getMachineToken( config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET - ); -}; + ) +} /** * Function to encode query string @@ -810,17 +828,17 @@ const getM2MUbahnToken = async () => { * @param {String} nesting the nesting string * @returns {String} query string */ -function encodeQueryString(queryObj, nesting = '') { +function encodeQueryString (queryObj, nesting = '') { const pairs = Object.entries(queryObj).map(([key, val]) => { // Handle the nested, recursive case, where the value to encode is an object itself if (typeof val === 'object') { - return encodeQueryString(val, nesting + `${key}.`); + return encodeQueryString(val, nesting + `${key}.`) } else { // Handle base case, where the value to encode is simply a string. - return [nesting + key, val].map(querystring.escape).join('='); + return [nesting + key, val].map(querystring.escape).join('=') } - }); - return pairs.join('&'); + }) + return pairs.join('&') } /** @@ -828,31 +846,31 @@ function encodeQueryString(queryObj, nesting = '') { * @param {Integer} externalId the legacy user id * @returns {Array} the users found */ -async function listUsersByExternalId(externalId) { +async function listUsersByExternalId (externalId) { // return empty list if externalId is null or undefined if (!!externalId !== true) { - return []; + return [] } - const token = await getM2MUbahnToken(); + const token = await getM2MUbahnToken() const q = { enrich: true, externalProfile: { organizationId: config.ORG_ID, - externalId, - }, - }; - const url = `${config.TC_API}/users?${encodeQueryString(q)}`; + externalId + } + } + const url = `${config.TC_API}/users?${encodeQueryString(q)}` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listUserByExternalId', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -860,14 +878,14 @@ async function listUsersByExternalId(externalId) { * @param {Integer} externalId the legacy user id * @returns {Object} the user */ -async function getUserByExternalId(externalId) { - const users = await listUsersByExternalId(externalId); +async function getUserByExternalId (externalId) { + const users = await listUsersByExternalId(externalId) if (_.isEmpty(users)) { throw new errors.NotFoundError( `externalId: ${externalId} "user" not found` - ); + ) } - return users[0]; + return users[0] } /** @@ -876,24 +894,24 @@ async function getUserByExternalId(externalId) { * @params {Object} payload the payload * @params {Object} options the extra options to control the function */ -async function postEvent(topic, payload, options = {}) { +async function postEvent (topic, payload, options = {}) { logger.debug({ component: 'helper', context: 'postEvent', message: `Posting event to Kafka topic ${topic}, ${JSON.stringify( payload - )}`, - }); - const client = getBusApiClient(); + )}` + }) + const client = getBusApiClient() const message = { topic, originator: config.KAFKA_MESSAGE_ORIGINATOR, timestamp: new Date().toISOString(), 'mime-type': 'application/json', - payload, - }; - await client.postEvent(message); - await eventDispatcher.handleEvent(topic, { value: payload, options }); + payload + } + await client.postEvent(message) + await eventDispatcher.handleEvent(topic, { value: payload, options }) } /** @@ -902,11 +920,11 @@ async function postEvent(topic, payload, options = {}) { * @param {Object} err the err * @returns {Boolean} the result */ -function isDocumentMissingException(err) { +function isDocumentMissingException (err) { if (err.statusCode === 404 && err instanceof ESResponseError) { - return true; + return true } - return false; + return false } /** @@ -915,34 +933,34 @@ function isDocumentMissingException(err) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getProjects(currentUser, criteria = {}) { - let token; +async function getProjects (currentUser, criteria = {}) { + let token if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken(); - token = `Bearer ${m2mToken}`; + const m2mToken = await getM2MToken() + token = `Bearer ${m2mToken}` } else { - token = currentUser.jwtToken; + token = currentUser.jwtToken } - const url = `${config.TC_API}/projects?type=talent-as-a-service`; + const url = `${config.TC_API}/projects?type=talent-as-a-service` const res = await request .get(url) .query(criteria) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getProjects', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) const result = _.map(res.body, (item) => { - return _.pick(item, ['id', 'name', 'invites', 'members']); - }); + return _.pick(item, ['id', 'name', 'invites', 'members']) + }) return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result, - }; + result + } } /** @@ -951,24 +969,24 @@ async function getProjects(currentUser, criteria = {}) { * @param {String} userId the legacy user id * @returns {Object} the user */ -async function getTopcoderUserById(userId) { - const token = await getM2MToken(); +async function getTopcoderUserById (userId) { + const token = await getM2MToken() const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `id=${userId}` }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getTopcoderUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - const user = _.get(res.body, 'result.content[0]'); + message: `response body: ${JSON.stringify(res.body)}` + }) + const user = _.get(res.body, 'result.content[0]') if (!user) { throw new errors.NotFoundError( `userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}` - ); + ) } - return user; + return user } /** @@ -976,31 +994,31 @@ async function getTopcoderUserById(userId) { * @param {String} userId the user id * @returns the request result */ -async function getUserById(userId, enrich) { - const token = await getM2MUbahnToken(); +async function getUserById (userId, enrich) { + const token = await getM2MUbahnToken() const res = await request .get(`${config.TC_API}/users/${userId}` + (enrich ? '?enrich=true' : '')) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) - const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']); + const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']) if (enrich) { user.skills = (res.body.skills || []).map((skillObj) => _.pick(skillObj.skill, ['id', 'name']) - ); - const attributes = _.get(res, 'body.attributes', []); + ) + const attributes = _.get(res, 'body.attributes', []) user.attributes = _.map(attributes, (attr) => _.pick(attr, ['id', 'value', 'attribute.id', 'attribute.name']) - ); + ) } - return user; + return user } /** @@ -1008,19 +1026,19 @@ async function getUserById(userId, enrich) { * @param {Object} data the user data * @returns the request result */ -async function createUbahnUser({ handle, firstName, lastName }) { - const token = await getM2MUbahnToken(); +async function createUbahnUser ({ handle, firstName, lastName }) { + const token = await getM2MUbahnToken() const res = await request .post(`${config.TC_API}/users`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ handle, firstName, lastName }); + .send({ handle, firstName, lastName }) localLogger.debug({ context: 'createUbahnUser', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id']) } /** @@ -1028,21 +1046,21 @@ async function createUbahnUser({ handle, firstName, lastName }) { * @param {String} userId the user id(with uuid format) * @param {Object} data the profile data */ -async function createUserExternalProfile( +async function createUserExternalProfile ( userId, { organizationId, externalId } ) { - const token = await getM2MUbahnToken(); + const token = await getM2MUbahnToken() const res = await request .post(`${config.TC_API}/users/${userId}/externalProfiles`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ organizationId, externalId: String(externalId) }); + .send({ organizationId, externalId: String(externalId) }) localLogger.debug({ context: 'createUserExternalProfile', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) } /** @@ -1050,23 +1068,23 @@ async function createUserExternalProfile( * @param {Array} handles the handle array * @returns the request result */ -async function getMembers(handles) { - const token = await getM2MToken(); +async function getMembers (handles) { + const token = await getM2MToken() const handlesStr = _.map(handles, (handle) => { - return '%22' + handle.toLowerCase() + '%22'; - }).join(','); - const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]`; + return '%22' + handle.toLowerCase() + '%22' + }).join(',') + const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getMembers', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -1075,36 +1093,36 @@ async function getMembers(handles) { * @param {Number} id project id * @returns the request result */ -async function getProjectById(currentUser, id) { - let token; +async function getProjectById (currentUser, id) { + let token if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken(); - token = `Bearer ${m2mToken}`; + const m2mToken = await getM2MToken() + token = `Bearer ${m2mToken}` } else { - token = currentUser.jwtToken; + token = currentUser.jwtToken } - const url = `${config.TC_API}/projects/${id}`; + const url = `${config.TC_API}/projects/${id}` try { const res = await request .get(url) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getProjectById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id', 'name', 'invites', 'members']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id', 'name', 'invites', 'members']) } catch (err) { if (err.status === HttpStatus.FORBIDDEN) { throw new errors.ForbiddenError( `You are not allowed to access the project with id ${id}` - ); + ) } if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${id} project not found`); + throw new errors.NotFoundError(`id: ${id} project not found`) } - throw err; + throw err } } @@ -1115,33 +1133,33 @@ async function getProjectById(currentUser, id) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getTopcoderSkills(criteria) { - const token = await getM2MUbahnToken(); +async function getTopcoderSkills (criteria) { + const token = await getM2MUbahnToken() try { const res = await request .get(`${config.TC_API}/skills`) .query({ skillProviderId: config.TOPCODER_SKILL_PROVIDER_ID, - ...criteria, + ...criteria }) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getTopcoderSkills', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result: res.body, - }; + result: res.body + } } catch (err) { if (err.status === HttpStatus.BAD_REQUEST) { - throw new errors.BadRequestError(err.response.body.message); + throw new errors.BadRequestError(err.response.body.message) } - throw err; + throw err } } @@ -1150,18 +1168,18 @@ async function getTopcoderSkills(criteria) { * @param {String} skillId the skill Id * @returns the request result */ -async function getSkillById(skillId) { - const token = await getM2MUbahnToken(); +async function getSkillById (skillId) { + const token = await getM2MUbahnToken() const res = await request .get(`${config.TC_API}/skills/${skillId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getSkillById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id', 'name']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id', 'name']) } /** @@ -1174,22 +1192,22 @@ async function getSkillById(skillId) { * @params {Object} currentUser the user who perform this operation * @returns {String} the ubahn user id */ -async function ensureUbahnUserId(currentUser) { +async function ensureUbahnUserId (currentUser) { try { - return (await getUserByExternalId(currentUser.userId)).id; + return (await getUserByExternalId(currentUser.userId)).id } catch (err) { if (!(err instanceof errors.NotFoundError)) { - throw err; + throw err } - const topcoderUser = await getTopcoderUserById(currentUser.userId); + const topcoderUser = await getTopcoderUserById(currentUser.userId) const user = await createUbahnUser( _.pick(topcoderUser, ['handle', 'firstName', 'lastName']) - ); + ) await createUserExternalProfile(user.id, { organizationId: config.ORG_ID, - externalId: currentUser.userId, - }); - return user.id; + externalId: currentUser.userId + }) + return user.id } } @@ -1199,8 +1217,8 @@ async function ensureUbahnUserId(currentUser) { * @param {String} jobId the job id * @returns {Object} the job data */ -async function ensureJobById(jobId) { - return models.Job.findById(jobId); +async function ensureJobById (jobId) { + return models.Job.findById(jobId) } /** @@ -1209,8 +1227,8 @@ async function ensureJobById(jobId) { * @param {String} resourceBookingId the resourceBooking id * @returns {Object} the resourceBooking data */ -async function ensureResourceBookingById(resourceBookingId) { - return models.ResourceBooking.findById(resourceBookingId); +async function ensureResourceBookingById (resourceBookingId) { + return models.ResourceBooking.findById(resourceBookingId) } /** @@ -1218,8 +1236,8 @@ async function ensureResourceBookingById(resourceBookingId) { * @param {String} workPeriodId the workPeriod id * @returns the workPeriod data */ -async function ensureWorkPeriodById(workPeriodId) { - return models.WorkPeriod.findById(workPeriodId); +async function ensureWorkPeriodById (workPeriodId) { + return models.WorkPeriod.findById(workPeriodId) } /** @@ -1228,24 +1246,24 @@ async function ensureWorkPeriodById(workPeriodId) { * @param {String} jobId the user id * @returns {Object} the user data */ -async function ensureUserById(userId) { - const token = await getM2MUbahnToken(); +async function ensureUserById (userId) { + const token = await getM2MUbahnToken() try { const res = await request .get(`${config.TC_API}/users/${userId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'ensureUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${userId} "user" not found`); + throw new errors.NotFoundError(`id: ${userId} "user" not found`) } - throw err; + throw err } } @@ -1254,12 +1272,12 @@ async function ensureUserById(userId) { * * @returns {Object} the M2M auth user */ -function getAuditM2Muser() { +function getAuditM2Muser () { return { isMachine: true, userId: config.m2m.M2M_AUDIT_USER_ID, - handle: config.m2m.M2M_AUDIT_HANDLE, - }; + handle: config.m2m.M2M_AUDIT_HANDLE + } } /** @@ -1271,24 +1289,24 @@ function getAuditM2Muser() { * @param {Number} projectId project id * @returns the result */ -async function checkIsMemberOfProject(userId, projectId) { - const m2mToken = await getM2MToken(); +async function checkIsMemberOfProject (userId, projectId) { + const m2mToken = await getM2MToken() const res = await request .get(`${config.TC_API}/projects/${projectId}`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const memberIdList = _.map(res.body.members, 'userId'); + .set('Accept', 'application/json') + const memberIdList = _.map(res.body.members, 'userId') localLogger.debug({ context: 'checkIsMemberOfProject', message: `the members of project ${projectId}: ${JSON.stringify( memberIdList - )}, authUserId: ${JSON.stringify(userId)}`, - }); + )}, authUserId: ${JSON.stringify(userId)}` + }) if (!memberIdList.includes(userId)) { throw new errors.UnauthorizedError( `userId: ${userId} the user is not a member of project ${projectId}` - ); + ) } } @@ -1298,24 +1316,24 @@ async function checkIsMemberOfProject(userId, projectId) { * @param {Array} handles the array of handles * @returns {Array} the member details */ -async function getMemberDetailsByHandles(handles) { +async function getMemberDetailsByHandles (handles) { if (!handles.length) { - return []; + return [] } - const token = await getM2MToken(); + const token = await getM2MToken() const res = await request .get(`${config.TOPCODER_MEMBERS_API}/`) .query({ 'handlesLower[]': handles.map(handle => handle.toLowerCase()), - fields: 'userId,handle,handleLower,firstName,lastName,email', + fields: 'userId,handle,handleLower,firstName,lastName,email' }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getMemberDetailsByHandles', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -1324,7 +1342,7 @@ async function getMemberDetailsByHandles(handles) { * @param {String} handle the user handle * @returns {Object} the member details */ -async function getMemberDetailsByHandle(handle) { +async function getMemberDetailsByHandle (handle) { const [memberDetails] = await getMemberDetailsByHandles([handle]) if (!memberDetails) { @@ -1341,20 +1359,20 @@ async function getMemberDetailsByHandle(handle) { * @param {String} email the email * @returns {Array} the member details */ -async function _getMemberDetailsByEmail(token, email) { +async function _getMemberDetailsByEmail (token, email) { const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `email=${email}`, - fields: 'handle,id,email,firstName,lastName', + fields: 'handle,id,email,firstName,lastName' }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: '_getMemberDetailsByEmail', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res.body, 'result.content'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res.body, 'result.content') } /** @@ -1364,25 +1382,25 @@ async function _getMemberDetailsByEmail(token, email) { * @param {Array} emails the array of emails * @returns {Array} the member details */ -async function getMemberDetailsByEmails(emails) { - const token = await getM2MToken(); +async function getMemberDetailsByEmails (emails) { + const token = await getM2MToken() const limiter = new Bottleneck({ - maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API, - }); + maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API + }) const membersArray = await Promise.all( emails.map((email) => limiter.schedule(() => _getMemberDetailsByEmail(token, email).catch((error) => { localLogger.error({ context: 'getMemberDetailsByEmails', - message: error.message, - }); - return []; + message: error.message + }) + return [] }) ) ) - ); - return _.flatten(membersArray); + ) + return _.flatten(membersArray) } /** @@ -1393,20 +1411,20 @@ async function getMemberDetailsByEmails(emails) { * @param {Object} criteria the filtering criteria * @returns {Object} the member created */ -async function createProjectMember(projectId, data, criteria) { - const m2mToken = await getM2MToken(); +async function createProjectMember (projectId, data, criteria) { + const m2mToken = await getM2MToken() const { body: member } = await request .post(`${config.TC_API}/projects/${projectId}/members`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') .query(criteria) - .send(data); + .send(data) localLogger.debug({ context: 'createProjectMember', - message: `response body: ${JSON.stringify(member)}`, - }); - return member; + message: `response body: ${JSON.stringify(member)}` + }) + return member } /** @@ -1416,21 +1434,21 @@ async function createProjectMember(projectId, data, criteria) { * @param {Object} criteria the search criteria * @returns {Array} the project members */ -async function listProjectMembers(currentUser, projectId, criteria = {}) { +async function listProjectMembers (currentUser, projectId, criteria = {}) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken const { body: members } = await request .get(`${config.TC_API}/projects/${projectId}/members`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listProjectMembers', - message: `response body: ${JSON.stringify(members)}`, - }); - return members; + message: `response body: ${JSON.stringify(members)}` + }) + return members } /** @@ -1440,21 +1458,21 @@ async function listProjectMembers(currentUser, projectId, criteria = {}) { * @param {Object} criteria the search criteria * @returns {Array} the member invites */ -async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { +async function listProjectMemberInvites (currentUser, projectId, criteria = {}) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken const { body: invites } = await request .get(`${config.TC_API}/projects/${projectId}/invites`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listProjectMemberInvites', - message: `response body: ${JSON.stringify(invites)}`, - }); - return invites; + message: `response body: ${JSON.stringify(invites)}` + }) + return invites } /** @@ -1464,24 +1482,24 @@ async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { * @param {String} projectMemberId the id of the project member * @returns {undefined} */ -async function deleteProjectMember(currentUser, projectId, projectMemberId) { +async function deleteProjectMember (currentUser, projectId, projectMemberId) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken try { await request .delete( `${config.TC_API}/projects/${projectId}/members/${projectMemberId}` ) - .set('Authorization', token); + .set('Authorization', token) } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { throw new errors.NotFoundError( `projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}` - ); + ) } - throw err; + throw err } } @@ -1491,13 +1509,13 @@ async function deleteProjectMember(currentUser, projectId, projectMemberId) { * @param {String} attributeName Requested attribute name, e.g. "email" * @returns attribute value */ -function getUserAttributeValue(user, attributeName) { - const attributes = _.get(user, 'attributes', []); +function getUserAttributeValue (user, attributeName) { + const attributes = _.get(user, 'attributes', []) const targetAttribute = _.find( attributes, (a) => a.attribute.name === attributeName - ); - return _.get(targetAttribute, 'value'); + ) + return _.get(targetAttribute, 'value') } /** @@ -1507,34 +1525,34 @@ function getUserAttributeValue(user, attributeName) { * @param {String} token m2m token * @returns {Object} the challenge created */ -async function createChallenge(data, token) { +async function createChallenge (data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/challenges`; + const url = `${config.TC_API}/challenges` localLogger.debug({ context: 'createChallenge', - message: `EndPoint: POST ${url}`, - }); + message: `EndPoint: POST ${url}` + }) localLogger.debug({ context: 'createChallenge', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: challenge, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createChallenge', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'createChallenge', - message: `Response Body: ${JSON.stringify(challenge)}`, - }); - return challenge; + message: `Response Body: ${JSON.stringify(challenge)}` + }) + return challenge } /** @@ -1545,34 +1563,34 @@ async function createChallenge(data, token) { * @param {String} token m2m token * @returns {Object} the challenge updated */ -async function updateChallenge(challengeId, data, token) { +async function updateChallenge (challengeId, data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/challenges/${challengeId}`; + const url = `${config.TC_API}/challenges/${challengeId}` localLogger.debug({ context: 'updateChallenge', - message: `EndPoint: PATCH ${url}`, - }); + message: `EndPoint: PATCH ${url}` + }) localLogger.debug({ context: 'updateChallenge', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: challenge, status: httpStatus } = await request .patch(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'updateChallenge', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'updateChallenge', - message: `Response Body: ${JSON.stringify(challenge)}`, - }); - return challenge; + message: `Response Body: ${JSON.stringify(challenge)}` + }) + return challenge } /** @@ -1582,34 +1600,34 @@ async function updateChallenge(challengeId, data, token) { * @param {String} token m2m token * @returns {Object} the resource created */ -async function createChallengeResource(data, token) { +async function createChallengeResource (data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/resources`; + const url = `${config.TC_API}/resources` localLogger.debug({ context: 'createChallengeResource', - message: `EndPoint: POST ${url}`, - }); + message: `EndPoint: POST ${url}` + }) localLogger.debug({ context: 'createChallengeResource', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: resource, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createChallengeResource', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'createChallengeResource', - message: `Response Body: ${JSON.stringify(resource)}`, - }); - return resource; + message: `Response Body: ${JSON.stringify(resource)}` + }) + return resource } /** @@ -1618,40 +1636,40 @@ async function createChallengeResource(data, token) { * @param {Date} end end date of the resource booking * @returns {Array<{startDate:Date, endDate:Date, daysWorked:number}>} information about workPeriods */ -function extractWorkPeriods(start, end) { +function extractWorkPeriods (start, end) { // calculate maximum possible daysWorked for a week - function getDaysWorked(week) { + function getDaysWorked (week) { if (weeks === 1) { - return Math.min(endDay, 5) - Math.max(startDay, 1) + 1; + return Math.min(endDay, 5) - Math.max(startDay, 1) + 1 } else if (week === 0) { - return Math.min(6 - startDay, 5); + return Math.min(6 - startDay, 5) } else if (week === weeks - 1) { - return Math.min(endDay, 5); - } else return 5; + return Math.min(endDay, 5) + } else return 5 } - const periods = []; + const periods = [] if (_.isNil(start) || _.isNil(end)) { - return periods; + return periods } - const startDate = moment(start); - const startDay = startDate.get('day'); - startDate.set('day', 0).startOf('day'); + const startDate = moment(start) + const startDay = startDate.get('day') + startDate.set('day', 0).startOf('day') - const endDate = moment(end); - const endDay = endDate.get('day'); - endDate.set('day', 6).endOf('day'); + const endDate = moment(end) + const endDay = endDate.get('day') + endDate.set('day', 6).endOf('day') - const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7; + const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7 for (let i = 0; i < weeks; i++) { periods.push({ startDate: startDate.format('YYYY-MM-DD'), endDate: startDate.add(6, 'day').format('YYYY-MM-DD'), - daysWorked: getDaysWorked(i), - }); - startDate.add(1, 'day'); + daysWorked: getDaysWorked(i) + }) + startDate.add(1, 'day') } - return periods; + return periods } /** @@ -1660,19 +1678,19 @@ function extractWorkPeriods(start, end) { * @param {String} userHandle user handle * @returns {String} email address of the user */ -async function getUserByHandle(userHandle) { - const token = await getM2MToken(); - const url = `${config.TC_API}/members/${userHandle}`; +async function getUserByHandle (userHandle) { + const token = await getM2MToken() + const url = `${config.TC_API}/members/${userHandle}` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getUserByHandle', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res, 'body'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res, 'body') } /** @@ -1681,14 +1699,14 @@ async function getUserByHandle(userHandle) { * @param {*} object of json that would be replaced in string * @returns */ -async function substituteStringByObject(string, object) { +async function substituteStringByObject (string, object) { for (var key in object) { if (!Object.prototype.hasOwnProperty.call(object, key)) { - continue; + continue } - string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]); + string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]) } - return string; + return string } /** @@ -1696,19 +1714,19 @@ async function substituteStringByObject(string, object) { * @param {Object} data title of project and any other info * @returns {Object} the project created */ -async function createProject(currentUser, data) { - const token = currentUser.jwtToken; +async function createProject (currentUser, data) { + const token = currentUser.jwtToken const res = await request .post(`${config.TC_API}/projects/`) .set('Authorization', token) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createProject', - message: `response body: ${JSON.stringify(res)}`, - }); - return _.get(res, 'body'); + message: `response body: ${JSON.stringify(res)}` + }) + return _.get(res, 'body') } module.exports = { @@ -1727,9 +1745,9 @@ module.exports = { getUserId: async (userId) => { // check m2m user id if (userId === config.m2m.M2M_AUDIT_USER_ID) { - return config.m2m.M2M_AUDIT_USER_ID; + return config.m2m.M2M_AUDIT_USER_ID } - return ensureUbahnUserId({ userId }); + return ensureUbahnUserId({ userId }) }, getUserByExternalId, getM2MToken, @@ -1763,5 +1781,5 @@ module.exports = { extractWorkPeriods, getUserByHandle, substituteStringByObject, - createProject, -}; + createProject +} diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index f5c40206..8871de9c 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -472,7 +472,6 @@ async function searchResourceBookings (currentUser, criteria, options = { return criteria.sortOrder = 'desc' } try { - throw new Error('fallback to DB') const esQuery = { index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), _source_includes: queryOpt.include, @@ -484,12 +483,10 @@ async function searchResourceBookings (currentUser, criteria, options = { return } }, from: (page - 1) * perPage, - size: perPage + size: perPage, + sort: [] } } - if (!queryOpt.sortByWP) { - esQuery.body.sort = [{ [criteria.sortBy === 'id' ? '_id' : criteria.sortBy]: { order: criteria.sortOrder } }] - } // change the date format to match with index schema if (criteria.startDate) { criteria.startDate = moment(criteria.startDate).format('YYYY-MM-DD') @@ -503,6 +500,17 @@ async function searchResourceBookings (currentUser, criteria, options = { return 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({ @@ -522,10 +530,10 @@ async function searchResourceBookings (currentUser, criteria, options = { return }] } // Apply WorkPeriod filters - const workPeriodFilters = ['workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle'] - if (_.intersection(criteria, workPeriodFilters).length > 0) { + const workPeriodFilters = _.pick(criteria, ['workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle']) + if (!_.isEmpty(workPeriodFilters)) { const workPeriodsMust = [] - _.each(_.pick(criteria, workPeriodFilters), (value, key) => { + _.each(workPeriodFilters, (value, key) => { workPeriodsMust.push({ term: { [key]: { @@ -545,23 +553,16 @@ async function searchResourceBookings (currentUser, criteria, options = { return logger.debug({ component: 'ResourceBookingService', context: 'searchResourceBookings', message: `Query: ${JSON.stringify(esQuery)}` }) const { body } = await esClient.search(esQuery) - let resourceBookings = _.map(body.hits.hits, '_source') + const resourceBookings = _.map(body.hits.hits, '_source') // ESClient will return ResourceBookings with it's all nested WorkPeriods // We re-apply WorkPeriod filters - _.each(_.pick(criteria, workPeriodFilters), (value, key) => { + _.each(workPeriodFilters, (value, key) => { key = key.split('.')[1] _.each(resourceBookings, r => { r.workPeriods = _.filter(r.workPeriods, { [key]: value }) }) }) - // If sorting criteria is WorkPeriod field, we have to sort manually - if (queryOpt.sortByWP) { - const sorts = criteria.sortBy.split('.') - resourceBookings = _.sortBy(resourceBookings, [`${sorts[0]}[0].${sorts[1]}`]) - if (criteria.sortOrder === 'desc') { - resourceBookings = _.reverse(resourceBookings) - } - } + return { total: body.hits.total.value, page, @@ -614,18 +615,26 @@ async function searchResourceBookings (currentUser, criteria, options = { return } // Apply sorting criteria if (!queryOpt.sortByWP) { - queryCriteria.order = [[criteria.sortBy, criteria.sortOrder]] + queryCriteria.order = [[criteria.sortBy, `${criteria.sortOrder} NULLS LAST`]] } else { - queryCriteria.order = [[{ model: WorkPeriod, as: 'workPeriods' }, _.split(criteria.sortBy, '.')[1], criteria.sortOrder]] - } - const resourceBookings = await ResourceBooking.findAll(queryCriteria) - const total = await ResourceBooking.count(_.omit(queryCriteria, ['limit', 'offset', 'attributes', 'order'])) + queryCriteria.subQuery = false + queryCriteria.order = [[{ model: WorkPeriod, as: 'workPeriods' }, _.split(criteria.sortBy, '.')[1], `${criteria.sortOrder} NULLS LAST`]] + } + const result = await ResourceBooking.findAll(queryCriteria) + let countQuery + countQuery = _.omit(queryCriteria, ['limit', 'offset', 'attributes', 'order']) + if (queryOpt.withWorkPeriods && !queryCriteria.include[0].required) { + countQuery = _.omit(countQuery, ['include']) + } + countQuery.subQuery = false + countQuery.group = ['ResourceBooking.id'] + const total = await ResourceBooking.count(countQuery) return { fromDb: true, - total, + total: total.length, page, perPage, - result: resourceBookings + result } } From 83b994c2dfdc857971c3af9491d268b0144f9443 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Mon, 31 May 2021 18:18:37 +0300 Subject: [PATCH 11/23] role endpoint added --- README.md | 6 +- app-constants.js | 8 +- config/default.js | 9 + data/demo-data.json | 260 +- ...coder-bookings-api.postman_collection.json | 3771 ++++++- docs/swagger.yaml | 476 + ...topcoder-bookings.postman_environment.json | 56 +- local/kafka-client/topics.txt | 3 + migrations/2021-05-27-1-role-table-create.js | 146 + .../2021-05-27-2-job-add-roleIds-field.js | 19 + package.json | 1 + scripts/data/exportData.js | 2 +- scripts/data/importData.js | 2 +- scripts/es/createIndex.js | 3 +- scripts/es/deleteIndex.js | 3 +- scripts/es/reIndexAll.js | 1 + scripts/es/reIndexRoles.js | 37 + src/bootstrap.js | 3 +- src/common/helper.js | 1115 +- src/controllers/RoleController.js | 59 + src/controllers/TeamController.js | 62 +- src/eventHandlers/RoleEventHandler.js | 64 + src/eventHandlers/index.js | 4 +- src/models/Job.js | 6 + src/models/Role.js | 165 + src/routes/RoleRoutes.js | 41 + src/routes/TeamRoutes.js | 48 +- src/services/InterviewService.js | 4 +- src/services/JobService.js | 42 +- src/services/ResourceBookingService.js | 1 + src/services/RoleService.js | 305 + src/services/TeamService.js | 390 +- taas-apis.patch | 9418 +++++++++++++++++ 33 files changed, 15661 insertions(+), 869 deletions(-) create mode 100644 migrations/2021-05-27-1-role-table-create.js create mode 100644 migrations/2021-05-27-2-job-add-roleIds-field.js create mode 100644 scripts/es/reIndexRoles.js create mode 100644 src/controllers/RoleController.js create mode 100644 src/eventHandlers/RoleEventHandler.js create mode 100644 src/models/Role.js create mode 100644 src/routes/RoleRoutes.js create mode 100644 src/services/RoleService.js create mode 100644 taas-apis.patch diff --git a/README.md b/README.md index 5e3895c2..aa36c621 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,9 @@ tc-taas-es-processor | [2021-04-09T21:20:19.035Z] app INFO : Starting kafka consumer tc-taas-es-processor | 2021-04-09T21:20:21.292Z INFO no-kafka-client Joined group taas-es-processor generationId 1 as no-kafka-client-076538fc-60dd-4ca4-a2b9-520bdf73bc9e tc-taas-es-processor | 2021-04-09T21:20:21.293Z INFO no-kafka-client Elected as group leader + tc-taas-es-processor | 2021-04-09T21:20:21.449Z DEBUG no-kafka-client Subscribed to taas.role.update:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-04-09T21:20:21.450Z DEBUG no-kafka-client Subscribed to taas.role.delete:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-04-09T21:20:21.451Z DEBUG no-kafka-client Subscribed to taas.role.requested:0 offset 0 leader kafka:9093 tc-taas-es-processor | 2021-04-09T21:20:21.452Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.create:0 offset 0 leader kafka:9093 tc-taas-es-processor | 2021-04-09T21:20:21.455Z DEBUG no-kafka-client Subscribed to taas.job.create:0 offset 0 leader kafka:9093 tc-taas-es-processor | 2021-04-09T21:20:21.456Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.delete:0 offset 0 leader kafka:9093 @@ -103,7 +106,7 @@ tc-taas-es-processor | 2021-04-09T21:20:21.473Z DEBUG no-kafka-client Subscribed to taas.job.update:0 offset 0 leader kafka:9093 tc-taas-es-processor | 2021-04-09T21:20:21.474Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.update:0 offset 0 leader kafka:9093 tc-taas-es-processor | [2021-04-09T21:20:21.475Z] app INFO : Initialized....... - tc-taas-es-processor | [2021-04-09T21:20:21.479Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete,taas.workperiod.create,taas.workperiod.update,taas.workperiod.delete,taas.workperiodpayment.create,taas.workperiodpayment.update,taas.workperiodpayment.delete + tc-taas-es-processor | [2021-04-09T21:20:21.479Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete,taas.workperiod.create,taas.workperiod.update,taas.workperiod.delete,taas.workperiodpayment.create,taas.workperiodpayment.update,taas.interview.requested,taas.interview.update,taas.interview.bulkUpdate,taas.role.requested,taas.role.update,taas.role.delete tc-taas-es-processor | [2021-04-09T21:20:21.480Z] app INFO : Kick Start....... tc-taas-es-processor | ********** Topcoder Health Check DropIn listening on port 3001 tc-taas-es-processor | Topcoder Health Check DropIn started and ready to roll @@ -194,6 +197,7 @@ To be able to change and test `taas-es-processor` locally you can follow the nex | `npm run index:jobs ` | Indexes job data from db into ES, if jobId is not given all data is indexed. Use `-- --force` flag to skip confirmation | | `npm run index:job-candidates ` | Indexes job candidate data from db into ES, if jobCandidateId is not given all data is indexed. Use `-- --force` flag to skip confirmation | | `npm run index:resource-bookings ` | Indexes resource bookings data from db into ES, if resourceBookingsId is not given all data is indexed. Use `-- --force` flag to skip confirmation | +| `npm run index:roles ` | Indexes roles data from db into ES, if roleId is not given all data is indexed. Use `-- --force` flag to skip confirmation | | `npm run services:up` | Start services via docker-compose for local development. | | `npm run services:down` | Stop services via docker-compose for local development. | | `npm run services:logs -- -f ` | View logs of some service inside docker-compose. | diff --git a/app-constants.js b/app-constants.js index 534e46de..9b577728 100644 --- a/app-constants.js +++ b/app-constants.js @@ -49,7 +49,13 @@ const Scopes = { READ_INTERVIEW: 'read:taas-interviews', CREATE_INTERVIEW: 'create:taas-interviews', UPDATE_INTERVIEW: 'update:taas-interviews', - ALL_INTERVIEW: 'all:taas-interviews' + ALL_INTERVIEW: 'all:taas-interviews', + // role + READ_ROLE: 'read:taas-roles', + CREATE_ROLE: 'create:taas-roles', + UPDATE_ROLE: 'update:taas-roles', + DELETE_ROLE: 'delete:taas-roles', + ALL_ROLE: 'all:taas-roles' } // Interview related constants diff --git a/config/default.js b/config/default.js index 2b5ca7ba..cf2a8a4f 100644 --- a/config/default.js +++ b/config/default.js @@ -76,6 +76,8 @@ module.exports = { ES_INDEX_JOB_CANDIDATE: process.env.ES_INDEX_JOB_CANDIDATE || 'job_candidate', // the resource booking index ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking', + // the role index + ES_INDEX_ROLE: process.env.ES_INDEX_ROLE || 'role', // the max bulk size in MB for ES indexing MAX_BULK_REQUEST_SIZE_MB: process.env.MAX_BULK_REQUEST_SIZE_MB || 20, @@ -131,6 +133,13 @@ module.exports = { TAAS_INTERVIEW_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_UPDATE_TOPIC || 'taas.interview.update', // the interview bulk update Kafka message topic TAAS_INTERVIEW_BULK_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_BULK_UPDATE_TOPIC || 'taas.interview.bulkUpdate', + // topics for role service + // the create role entity Kafka message topic + TAAS_ROLE_CREATE_TOPIC: process.env.TAAS_ROLE_CREATE_TOPIC || 'taas.role.requested', + // the update role entity Kafka message topic + TAAS_ROLE_UPDATE_TOPIC: process.env.TAAS_ROLE_UPDATE_TOPIC || 'taas.role.update', + // the delete role entity Kafka message topic + TAAS_ROLE_DELETE_TOPIC: process.env.TAAS_ROLE_DELETE_TOPIC || 'taas.role.delete', // the Kafka message topic for sending email EMAIL_TOPIC: process.env.EMAIL_TOPIC || 'external.action.email', diff --git a/data/demo-data.json b/data/demo-data.json index e0733443..5f6c4c07 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -20,6 +20,7 @@ ], "status": "in-review", "isApplicationPageActive": false, + "roleIds": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:21:10.394Z", @@ -45,6 +46,7 @@ ], "status": "in-review", "isApplicationPageActive": false, + "roleIds": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:11:26.934Z", @@ -70,6 +72,7 @@ ], "status": "in-review", "isApplicationPageActive": false, + "roleIds": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:18.595Z", @@ -95,6 +98,7 @@ ], "status": "in-review", "isApplicationPageActive": false, + "roleIds": null, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:12:09.293Z", @@ -181,18 +185,29 @@ "interviews": [ { "id": "077aa2ca-5b60-4ad9-a965-1b37e08a5046", + "xaiId": null, "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", - "googleCalendarId": null, - "customMessage": null, - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 1, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Completed", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:16:10.887Z", - "updatedAt": "2021-05-09T21:16:10.887Z" + "updatedAt": "2021-05-09T21:16:10.887Z", + "deletedAt": null } ] }, @@ -210,33 +225,55 @@ "interviews": [ { "id": "b1f7ba76-640f-47e2-9463-59e51b51ec60", + "xaiId": null, "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 2, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Scheduling", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:17:23.517Z", - "updatedAt": "2021-05-09T21:17:23.517Z" + "updatedAt": "2021-05-09T21:17:23.517Z", + "deletedAt": null }, { "id": "3144fa65-ea1a-4bec-81b0-7cb1c8845826", + "xaiId": null, "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": null, - "customMessage": null, - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 1, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Completed", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:16:39.019Z", - "updatedAt": "2021-05-09T21:16:39.019Z" + "updatedAt": "2021-05-09T21:16:39.019Z", + "deletedAt": null } ] }, @@ -254,54 +291,81 @@ "interviews": [ { "id": "976d23a9-5710-453f-99d9-f57a588bb610", + "xaiId": null, "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 3, "startTimestamp": null, - "attendeesList": [ - "attendee1@yopmail.com", - "attendee2@yopmail.com" - ], + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Scheduling", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:21:28.713Z", - "updatedAt": "2021-05-09T21:21:28.713Z" + "updatedAt": "2021-05-09T21:21:28.713Z", + "deletedAt": null }, { "id": "a23e1bf2-1084-4cfe-a0d8-d83bc6fec655", + "xaiId": null, "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 2, "startTimestamp": null, - "attendeesList": [ - "attendee1@yopmail.com", - "attendee2@yopmail.com" - ], + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Scheduling", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:21:22.428Z", - "updatedAt": "2021-05-09T21:21:22.428Z" + "updatedAt": "2021-05-09T21:21:22.428Z", + "deletedAt": null }, { "id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb", + "xaiId": null, "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": null, - "customMessage": null, - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 1, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Completed", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:21:17.346Z", - "updatedAt": "2021-05-09T21:21:17.346Z" + "updatedAt": "2021-05-09T21:21:17.346Z", + "deletedAt": null } ] }, @@ -2052,5 +2116,127 @@ } ] } + ], + "Role": [ + { + "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.", + "listOfSkills": [ + "database", + "winforms", + "user interface (ui)", + "photoshop" + ], + "rates": [ + { + "global": 50, + "offShore": 10, + "inCountry": 20 + }, + { + "global": 25, + "offShore": 5, + "inCountry": 15 + } + ], + "numberOfMembers": "10", + "numberOfMembersAvailable": 8, + "imageUrl": "http://images.topcoder.com/member", + "timeToCandidate": 105, + "timeToInterview": 100, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-27T21:43:08.201Z", + "updatedAt": "2021-05-27T21:43:08.201Z" + }, + { + "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.", + "listOfSkills": [ + "dropwizard", + "nginx", + "machine learning", + "force.com" + ], + "rates": [ + { + "global": 50, + "offShore": 10, + "inCountry": 20, + "rate20Global": 20, + "rate30Global": 20, + "rate20OffShore": 35, + "rate30OffShore": 35, + "rate20InCountry": 15, + "rate30InCountry": 15 + }, + { + "global": 25, + "offShore": 5, + "inCountry": 15, + "rate20Global": 20, + "rate30Global": 20, + "rate20OffShore": 35, + "rate30OffShore": 35, + "rate20InCountry": 15, + "rate30InCountry": 15 + } + ], + "numberOfMembers": "10", + "numberOfMembersAvailable": 8, + "imageUrl": "http://images.topcoder.com/member", + "timeToCandidate": 105, + "timeToInterview": 100, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-27T21:43:04.717Z", + "updatedAt": "2021-05-27T21:43:04.717Z" + }, + { + "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.", + "listOfSkills": [ + "docker", + ".net", + "appcelerator", + "flux" + ], + "rates": [ + { + "global": 50, + "offShore": 10, + "inCountry": 20, + "rate20Global": 20, + "rate30Global": 20, + "rate20OffShore": 35, + "rate30OffShore": 35, + "rate20InCountry": 15, + "rate30InCountry": 15 + }, + { + "global": 25, + "offShore": 5, + "inCountry": 15, + "rate20Global": 20, + "rate30Global": 20, + "rate20OffShore": 35, + "rate30OffShore": 35, + "rate20InCountry": 15, + "rate30InCountry": 15 + } + ], + "numberOfMembers": "10", + "numberOfMembersAvailable": 6, + "imageUrl": "http://images.topcoder.com/member", + "timeToCandidate": 105, + "timeToInterview": 100, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": null, + "createdAt": "2021-05-27T21:43:09.342Z", + "updatedAt": "2021-05-27T21:43:09.342Z" + } ] -} +} \ No newline at end of file diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index a0518c50..39e8c0cb 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", + "_postman_id": "82ad9051-0f67-46dd-8875-cce9979a22f4", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -17816,6 +17816,2970 @@ } ] }, + { + "name": "Roles", + "item": [ + { + "name": "Create Role", + "item": [ + { + "name": "create role with admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + " if(pm.response.status === \"OK\"){\r", + " const response = pm.response.json()\r", + " pm.environment.set(\"roleId-1\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n }\n ],\n \"numberOfMembers\": 10,\n \"numberOfMembersAvailable\": 8,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with booking manager", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + " if(pm.response.status === \"OK\"){\r", + " const response = pm.response.json()\r", + " pm.environment.set(\"roleId-2\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Angular Developer\",\n \"description\": \"Angular is an open-source, client-side framework based on TypeScript and designed for building web applications.\",\n \"listOfSkills\": [\n \"Database\",\n \"Winforms\",\n \"User Interface (Ui)\",\n \"Photoshop\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"numberOfMembersAvailable\": 8,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with m2m create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + " if(pm.response.status === \"OK\"){\r", + " const response = pm.response.json()\r", + " pm.environment.set(\"roleId-3\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_create_role}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Salesforce Developer\",\n \"description\": \"A Salesforce developer is a programmer who builds Salesforce applications across various PaaS (Platform as a Service) platforms.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\",\n \"appcelerator\",\n \"Flux\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n }\n ],\n \"numberOfMembers\": 10,\n \"numberOfMembersAvailable\": 6,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer invalid_token" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with existent name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"Role: \\\"Dev Ops Engineer\\\" is already exists.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with missing parameter 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.name\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with missing parameter 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.rates\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with missing parameter 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.rates\\\" does not contain 1 required value(s)\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with missing parameter 4", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.rates[0].global\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with missing parameter 5", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.rates[0].inCountry\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with missing parameter 6", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.rates[0].offShore\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.name\\\" length must be less than or equal to 50 characters long\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.listOfSkills[0]\\\" length must be less than or equal to 50 characters long\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard\",\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.listOfSkills\\\" must be an array\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\":\"Dropwizard\",\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 4", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.rates\\\" must be an array\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 5", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.rates[0].global\\\" must be a number\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": \"first\",\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 6", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.rates[0].inCountry\\\" must be a number\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": \"fifty\",\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 7", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.numberOfMembers\\\" must be a number\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": null,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 8", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.imageUrl\\\" must be a valid uri\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 9", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"role.timeToCandidate\\\" must be less than or equal to 32767\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 99999,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 10", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"skills: \\\"teamworking,communication,problem-solving\\\" are not valid\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer 2\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 55,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Get Role", + "item": [ + { + "name": "get role with admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "get role with booking manager fromDb", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-2}}?fromDb=true", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-2}}" + ], + "query": [ + { + "key": "fromDb", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "get role with m2m read", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_read_role}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-3}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-3}}" + ] + } + }, + "response": [] + }, + { + "name": "get role with connect user fromDb", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}?fromDb=true", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ], + "query": [ + { + "key": "fromDb", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "get role with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-2}}" + ] + } + }, + "response": [] + }, + { + "name": "get role with invalid token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer invalid token" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-2}}" + ] + } + }, + "response": [] + }, + { + "name": "get role with invalid id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"id\\\" must be a valid GUID\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "url": { + "raw": "{{URL}}/roles/invalid", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "invalid" + ] + } + }, + "response": [] + }, + { + "name": "get role with missing id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 404', function () {\r", + " pm.response.to.have.status(404);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"id: 00000000-0000-0000-0000-000000000000 \\\"Role\\\" not found\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "url": { + "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "00000000-0000-0000-0000-000000000000" + ] + } + }, + "response": [] + }, + { + "name": "search roles with admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "search roles with booking manager", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/roles?skillsList=dropwizard, nginx,, machine learning , FORce.com &keyword=ops e", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ], + "query": [ + { + "key": "skillsList", + "value": "dropwizard, nginx,, machine learning , FORce.com " + }, + { + "key": "keyword", + "value": "ops e" + } + ] + } + }, + "response": [] + }, + { + "name": "search roles with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "url": { + "raw": "{{URL}}/roles?skillsList=dataBase, ,Photoshop&keyword=sale", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ], + "query": [ + { + "key": "skillsList", + "value": "dataBase, ,Photoshop" + }, + { + "key": "keyword", + "value": "sale" + } + ] + } + }, + "response": [] + }, + { + "name": "search roles with m2m read", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_read_role}}" + } + ], + "url": { + "raw": "{{URL}}/roles?skillsList=DOCKER,.NET&keyword=dev", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ], + "query": [ + { + "key": "skillsList", + "value": "DOCKER,.NET" + }, + { + "key": "keyword", + "value": "dev" + } + ] + } + }, + "response": [] + }, + { + "name": "search roles with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "url": { + "raw": "{{URL}}/roles?keyword=dev", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ], + "query": [ + { + "key": "keyword", + "value": "dev" + } + ] + } + }, + "response": [] + }, + { + "name": "search roles with invalid token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer invalid token" + } + ], + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Update Role", + "item": [ + { + "name": "update role with admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer edit\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with booking manager", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Angular Developer edit\",\n \"description\": \"Angular is an open-source, client-side framework based on TypeScript and designed for building web applications.\",\n \"listOfSkills\": [\n \"Database\",\n \"Winforms\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-2}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with m2m update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_update_role}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Salesforce Developer edit\",\n \"description\": \"A Salesforce developer is a programmer who builds Salesforce applications across various PaaS (Platform as a Service) platforms.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-3}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-3}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer invalid_token" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"id\\\" must be a valid GUID\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/invalid", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "invalid" + ] + } + }, + "response": [] + }, + { + "name": "update role with missing id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 404', function () {\r", + " pm.response.to.have.status(404);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"id: 00000000-0000-0000-0000-000000000000 \\\"Role\\\" doesn't exists.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "00000000-0000-0000-0000-000000000000" + ] + } + }, + "response": [] + }, + { + "name": "update role with existent name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"Role: \\\"Angular Developer edit\\\" is already exists.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Angular Developer edit\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"data.name\\\" length must be less than or equal to 50 characters long\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"data.listOfSkills[0]\\\" length must be less than or equal to 50 characters long\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking\",\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"data.listOfSkills\\\" must be an array\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\":\"Teamworking\",\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 4", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"data.rates\\\" must be an array\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 5", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"data.rates[0].global\\\" must be a number\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": \"first\",\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 6", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"data.rates[0].inCountry\\\" must be a number\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": \"fifty\",\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 7", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"data.numberOfMembers\\\" must be a number\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": \"hundred\",\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 8", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"data.imageUrl\\\" must be a valid uri\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 9", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"data.timeToCandidate\\\" must be less than or equal to 32767\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 99999,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 10", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"skills: \\\"teamworking\\\" are not valid\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 66,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Delete Role", + "item": [ + { + "name": "delete role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "delete role with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "delete role with invalid token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer invalid_token" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"teamworking\",\n \"communication\",\n \"problem-solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "delete role with invalid id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"\\\"id\\\" must be a valid GUID\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/invalid", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "invalid" + ] + } + }, + "response": [] + }, + { + "name": "delete role with missing id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 404', function () {\r", + " pm.response.to.have.status(404);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"id: 00000000-0000-0000-0000-000000000000 \\\"Role\\\" doesn't exists.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "00000000-0000-0000-0000-000000000000" + ] + } + }, + "response": [] + }, + { + "name": "delete role with admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204', function () {\r", + " pm.response.to.have.status(204);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "delete role with booking manager", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204', function () {\r", + " pm.response.to.have.status(204);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-2}}" + ] + } + }, + "response": [] + }, + { + "name": "delete role with m2m delete", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204', function () {\r", + " pm.response.to.have.status(204);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_delete_role}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-3}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-3}}" + ] + } + }, + "response": [] + } + ] + } + ] + }, { "name": "health check", "item": [ @@ -22399,7 +25363,226 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_by_administrator}}", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments", + "{{workPeriodPaymentId_created_by_administrator}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Roles", + "item": [ + { + "name": "✔ create role with admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + " if(pm.response.status === \"OK\"){\r", + " const response = pm.response.json()\r", + " pm.environment.set(\"roleId-1\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "✔ get role with admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "✔ search roles with admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "✔ update role with admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer edit\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "✔ delete role with admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204', function () {\r", + " pm.response.to.have.status(204);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "", "options": { "raw": { "language": "json" @@ -22407,13 +25590,13 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_by_administrator}}", + "raw": "{{URL}}/roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "work-period-payments", - "{{workPeriodPaymentId_created_by_administrator}}" + "roles", + "{{roleId-1}}" ] } }, @@ -24635,12 +27818,293 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_member_tester1234}}" + "value": "Bearer {{token_member_tester1234}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_for_member}}", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments", + "{{workPeriodPaymentId_created_for_member}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Roles", + "item": [ + { + "name": "Before Start", + "item": [ + { + "name": "✔ create role with admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + " if(pm.response.status === \"OK\"){\r", + " const response = pm.response.json()\r", + " pm.environment.set(\"roleId-1\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "✘ create role with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "✔ get role with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "✔ search roles with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "url": { + "raw": "{{URL}}/roles?keyword=Dev", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ], + "query": [ + { + "key": "keyword", + "value": "Dev" + } + ] + } + }, + "response": [] + }, + { + "name": "✘ update role with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "✘ delete role with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", + "raw": "", "options": { "raw": { "language": "json" @@ -24648,13 +28112,13 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_for_member}}", + "raw": "{{URL}}/roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "work-period-payments", - "{{workPeriodPaymentId_created_for_member}}" + "roles", + "{{roleId-1}}" ] } }, @@ -26894,10 +30358,295 @@ "response": [] } ] + }, + { + "name": "Roles", + "item": [ + { + "name": "Before Start", + "item": [ + { + "name": "✔ create role with admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + " if(pm.response.status === \"OK\"){\r", + " const response = pm.response.json()\r", + " pm.environment.set(\"roleId-1\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer 2\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "✘ create role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "✔ get role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "✔ search roles with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "url": { + "raw": "{{URL}}/roles?skillsList=Dropwizard, ,NGINX&keyword=Dev", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ], + "query": [ + { + "key": "skillsList", + "value": "Dropwizard, ,NGINX" + }, + { + "key": "keyword", + "value": "Dev" + } + ] + } + }, + "response": [] + }, + { + "name": "✘ update role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "✘ delete role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + } + ] } ] } ] } ] -} +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a0b6064b..e5f1ac26 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -18,6 +18,8 @@ tags: - name: ResourceBookings - name: Teams - name: WorkPeriods + - name: WorkPeriodPayments + - name: Roles paths: /jobs: post: @@ -3245,6 +3247,267 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /roles/new: + post: + tags: + - Roles + description: | + Create Role. + + **Authorization** Topcoder m2m token with create scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RoleRequestBody" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Role" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /roles: + get: + tags: + - Roles + description: | + Search roles. + + **Authorization** Topcoder m2m token with read scope is allowed. Topcoder user token with any role is allowed. + security: + - bearerAuth: [] + parameters: + - in: query + name: skillsList + required: false + schema: + type: string + description: comma separated skill names. case-insensitive. + - in: query + name: keyword + required: false + schema: + type: string + description: role name. case-insensitive. partial match allowed + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Role" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /roles/{id}: + get: + tags: + - Roles + description: | + Get role by id. + + **Authorization** Topcoder m2m token with read scope is allowed. Topcoder user token with any role is allowed. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + description: The role id. + required: true + schema: + type: string + format: uuid + - in: query + name: fromDb + description: get data from db or not. + required: false + schema: + type: boolean + default: false + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Role" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + patch: + tags: + - Roles + description: | + Partial Update role. + + **Authorization** Topcoder m2m token with update scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + description: The id of role. + required: true + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RolePatchRequestBody" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Role" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + delete: + tags: + - Roles + description: | + Delete the role. + + **Authorization** Topcoder m2m token with delete scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + description: The id of role. + required: true + schema: + type: string + format: uuid + responses: + "204": + description: OK + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /health: get: tags: @@ -3335,6 +3598,13 @@ components: type: string format: uuid description: "The skill id." + roleIds: + type: array + description: "The roles." + items: + type: string + format: uuid + description: "The role id." status: type: string enum: ["sourcing", "in-review", "assigned", "closed", "cancelled"] @@ -3424,6 +3694,13 @@ components: type: string format: uuid description: "The skill id." + roleIds: + type: array + description: "The roles." + items: + type: string + format: uuid + description: "The role id." isApplicationPageActive: type: boolean default: false @@ -3865,6 +4142,13 @@ components: type: string format: uuid description: "The skill id." + roleIds: + type: array + description: "The roles." + items: + type: string + format: uuid + description: "The role id." isApplicationPageActive: type: boolean default: false @@ -4710,6 +4994,198 @@ components: type: string description: "the email of a member" example: "xxx@xxx.com" + Role: + required: + - id + - name + - rates + - createdAt + - createdBy + properties: + id: + type: string + format: uuid + description: "The role id." + name: + type: string + example: "Dev Ops Engineer" + description: "The role name." + description: + type: string + example: "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: "The role description" + listOfSkills: + type: array + description: "The array of skill names." + items: + type: string + example: "HTML" + description: "The skill name" + rates: + type: array + description: "The rates object array." + items: + $ref: "#/components/schemas/RoleRates" + numberOfMembers: + type: number + example: 100 + description: "The number of members." + numberOfMembersAvailable: + type: integer + example: 100 + description: "The number of members available." + imageUrl: + type: string + format: url + example: "http://images.topcoder.com/images" + description: "The image url of the role." + timeToCandidate: + type: integer + example: 200 + description: "The time to candidate." + timeToInterview: + type: integer + example: 300 + description: "The time to interview." + createdAt: + type: string + format: date-time + description: "The role created date." + createdBy: + type: string + format: uuid + description: "The user Id who created the role.(Will get the user info from the token)" + updatedAt: + type: string + format: date-time + description: "The role last updated at." + updatedBy: + type: string + format: uuid + description: "The user Id who updated the role last time.(Will get the user info from the token)" + RoleRequestBody: + required: + - name + - rates + properties: + name: + type: string + example: "Dev Ops Engineer" + description: "The role name." + description: + type: string + example: "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: "The role description" + listOfSkills: + type: array + description: "The array of skill names." + items: + type: string + example: "HTML" + description: "The skill name" + rates: + type: array + description: "The rates object array." + items: + $ref: "#/components/schemas/RoleRates" + numberOfMembers: + type: number + example: 100 + description: "The number of members." + numberOfMembersAvailable: + type: number + example: 100 + description: "The number of members available." + imageUrl: + type: string + format: url + example: "http://images.topcoder.com/images" + description: "The image url of the role." + timeToCandidate: + type: integer + example: 200 + description: "The time to candidate." + timeToInterview: + type: integer + example: 300 + description: "The time to interview." + RolePatchRequestBody: + properties: + name: + type: string + example: "Dev Ops Engineer" + description: "The role name." + description: + type: string + example: "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: "The role description" + listOfSkills: + type: array + description: "The array of skill names." + items: + type: string + example: "HTML" + description: "The skill name" + rates: + type: array + description: "The rates object array." + items: + $ref: "#/components/schemas/RoleRates" + numberOfMembers: + type: number + example: 100 + description: "The number of members." + numberOfMembersAvailable: + type: number + example: 100 + description: "The number of members available." + imageUrl: + type: string + format: url + example: "http://images.topcoder.com/images" + description: "The image url of the role." + timeToCandidate: + type: integer + example: 200 + description: "The time to candidate." + timeToInterview: + type: integer + example: 300 + description: "The time to interview." + RoleRates: + required: + - global + - inCountry + - offShore + type: object + properties: + global: + type: integer + example: 10 + inCountry: + type: integer + example: 20 + offShore: + type: integer + example: 30 + rate30Global: + type: integer + example: 10 + rate30InCountry: + type: integer + example: 20 + rate30OffShore: + type: integer + example: 30 + rate20Global: + type: integer + example: 10 + rate20InCountry: + type: integer + example: 20 + rate20OffShore: + type: integer + example: 30 ProjectMember: type: object example: diff --git a/docs/topcoder-bookings.postman_environment.json b/docs/topcoder-bookings.postman_environment.json index 837b55db..c83fc9af 100644 --- a/docs/topcoder-bookings.postman_environment.json +++ b/docs/topcoder-bookings.postman_environment.json @@ -1,5 +1,5 @@ { - "id": "228f4dcc-6914-462e-9b56-3285b643a2f8", + "id": "0ce42def-1c70-4c24-8986-914caa57f3c8", "name": "topcoder-bookings", "values": [ { @@ -312,11 +312,6 @@ "value": "", "enabled": true }, - { - "key": "job_id_created_for_member", - "value": "", - "enabled": true - }, { "key": "resource_bookings_id_created_for_member", "value": "", @@ -327,11 +322,6 @@ "value": "", "enabled": true }, - { - "key": "job_id_created_for_connect_manager", - "value": "", - "enabled": true - }, { "key": "resource_bookings_id_created_for_connect_manager", "value": "", @@ -461,9 +451,49 @@ "key": "interview_id_created_for_connect_manager", "value": "", "enabled": true + }, + { + "key": "token_m2m_create_role", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.f1QP1QTacyDxy7dwzUhBIT8blXCjKn_mnu9Cg59vIc8", + "enabled": true + }, + { + "key": "token_m2m_read_role", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJyZWFkOnRhYXMtcm9sZXMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.ZeWS_W2o8YwlvIB_-z0CFFa9zhRjptCk7qNXsPPWxVY", + "enabled": true + }, + { + "key": "token_m2m_update_role", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJ1cGRhdGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.0t4k0skZmxAUKuHQrG3ZrO2dgWcDMLD8W1rVluCy7XQ", + "enabled": true + }, + { + "key": "token_m2m_delete_role", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJkZWxldGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.NSBbWOk5jCB8nIvLiZwJtR9px5wmUQaQjgpDlMDJ9hk", + "enabled": true + }, + { + "key": "token_m2m_all_role", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJhbGw6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.cn0QVTOFnbHJckYqmGcpUBT8wQUxXWwtteWU7uhlDtI", + "enabled": true + }, + { + "key": "roleId-1", + "value": "", + "enabled": true + }, + { + "key": "roleId-2", + "value": "", + "enabled": true + }, + { + "key": "roleId-3", + "value": "", + "enabled": true } ], "_postman_variable_scope": "environment", - "_postman_exported_at": "2021-05-10T05:06:38.661Z", - "_postman_exported_using": "Postman/8.3.1" + "_postman_exported_at": "2021-05-27T01:32:45.726Z", + "_postman_exported_using": "Postman/8.5.1" } \ No newline at end of file diff --git a/local/kafka-client/topics.txt b/local/kafka-client/topics.txt index 8766a1b5..760c3a82 100644 --- a/local/kafka-client/topics.txt +++ b/local/kafka-client/topics.txt @@ -3,16 +3,19 @@ taas.jobcandidate.create taas.resourcebooking.create taas.workperiod.create taas.workperiodpayment.create +taas.role.requested taas.job.update taas.jobcandidate.update taas.resourcebooking.update taas.workperiod.update taas.workperiodpayment.update +taas.role.update taas.job.delete taas.jobcandidate.delete taas.resourcebooking.delete taas.workperiod.delete taas.workperiodpayment.delete +taas.role.delete taas.interview.requested taas.interview.update taas.interview.bulkUpdate diff --git a/migrations/2021-05-27-1-role-table-create.js b/migrations/2021-05-27-1-role-table-create.js new file mode 100644 index 00000000..bce2ae17 --- /dev/null +++ b/migrations/2021-05-27-1-role-table-create.js @@ -0,0 +1,146 @@ +const config = require('config') + +/* + * Create role table + */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.createTable('roles', { + id: { + type: Sequelize.UUID, + primaryKey: true, + allowNull: false, + defaultValue: Sequelize.UUIDV4 + }, + name: { + type: Sequelize.STRING(50), + allowNull: false + }, + description: { + type: Sequelize.STRING(1000) + }, + listOfSkills: { + field: 'list_of_skills', + type: Sequelize.ARRAY({ + type: Sequelize.STRING(50) + }) + }, + rates: { + type: Sequelize.ARRAY({ + type: Sequelize.JSONB({ + global: { + type: Sequelize.SMALLINT, + allowNull: false + }, + inCountry: { + field: 'in_country', + type: Sequelize.SMALLINT, + allowNull: false + }, + offShore: { + field: 'off_shore', + type: Sequelize.SMALLINT, + allowNull: false + }, + rate30Global: { + field: 'rate30_global', + type: Sequelize.SMALLINT + }, + rate30InCountry: { + field: 'rate30_in_country', + type: Sequelize.SMALLINT + }, + rate30OffShore: { + field: 'rate30_off_shore', + type: Sequelize.SMALLINT + }, + rate20Global: { + field: 'rate20_global', + type: Sequelize.SMALLINT + }, + rate20InCountry: { + field: 'rate20_in_country', + type: Sequelize.SMALLINT + }, + rate20OffShore: { + field: 'rate20_off_shore', + type: Sequelize.SMALLINT + } + }), + allowNull: false + }), + allowNull: false + }, + numberOfMembers: { + field: 'number_of_members', + type: Sequelize.NUMERIC + }, + numberOfMembersAvailable: { + field: 'number_of_members_available', + type: Sequelize.SMALLINT + }, + imageUrl: { + field: 'image_url', + type: Sequelize.STRING(255) + }, + timeToCandidate: { + field: 'time_to_candidate', + type: Sequelize.SMALLINT + }, + timeToInterview: { + field: 'time_to_interview', + type: Sequelize.SMALLINT + }, + createdBy: { + field: 'created_by', + type: Sequelize.UUID, + allowNull: false + }, + updatedBy: { + field: 'updated_by', + type: Sequelize.UUID + }, + createdAt: { + field: 'created_at', + type: Sequelize.DATE + }, + updatedAt: { + field: 'updated_at', + type: Sequelize.DATE + }, + deletedAt: { + field: 'deleted_at', + type: Sequelize.DATE + } + }, { + schema: config.DB_SCHEMA_NAME, + transaction + }) + await queryInterface.addIndex( + { + tableName: 'roles', + schema: config.DB_SCHEMA_NAME + }, + ['name'], + { + type: 'UNIQUE', + where: { deleted_at: null }, + transaction: transaction + } + ) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable({ + tableName: 'roles', + schema: config.DB_SCHEMA_NAME + }) + } +} diff --git a/migrations/2021-05-27-2-job-add-roleIds-field.js b/migrations/2021-05-27-2-job-add-roleIds-field.js new file mode 100644 index 00000000..a5b9f4be --- /dev/null +++ b/migrations/2021-05-27-2-job-add-roleIds-field.js @@ -0,0 +1,19 @@ +const config = require('config') + +/* + * Add roleIds field to the Job model. + */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'role_ids', + { + type: Sequelize.ARRAY({ + type: Sequelize.UUID + }) + }) + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'role_ids') + } +} diff --git a/package.json b/package.json index 0fa24cca..510504f1 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "index:jobs": "node scripts/es/reIndexJobs.js", "index:job-candidates": "node scripts/es/reIndexJobCandidates.js", "index:resource-bookings": "node scripts/es/reIndexResourceBookings.js", + "index:roles": "node scripts/es/reIndexRoles.js", "data:export": "node scripts/data/exportData.js", "data:import": "node scripts/data/importData.js", "migrate": "npx sequelize db:migrate", diff --git a/scripts/data/exportData.js b/scripts/data/exportData.js index 4eee1ad5..cb61e582 100644 --- a/scripts/data/exportData.js +++ b/scripts/data/exportData.js @@ -28,7 +28,7 @@ const resourceBookingModelOpts = { const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH const userPrompt = `WARNING: are you sure you want to export all data in the database to a json file with the path ${filePath}? This will overwrite the file.` -const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts] +const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts, 'Role'] async function exportData () { await helper.promptUser(userPrompt, async () => { diff --git a/scripts/data/importData.js b/scripts/data/importData.js index 2e9c168e..a0aeeb64 100644 --- a/scripts/data/importData.js +++ b/scripts/data/importData.js @@ -28,7 +28,7 @@ const resourceBookingModelOpts = { const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH const userPrompt = `WARNING: this would remove existing data. Are you sure you want to import data from a json file with the path ${filePath}?` -const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts] +const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts, 'Role'] async function importData () { await helper.promptUser(userPrompt, async () => { diff --git a/scripts/es/createIndex.js b/scripts/es/createIndex.js index d2c72943..269cd5a4 100644 --- a/scripts/es/createIndex.js +++ b/scripts/es/createIndex.js @@ -8,7 +8,8 @@ const helper = require('../../src/common/helper') const indices = [ config.get('esConfig.ES_INDEX_JOB'), config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + config.get('esConfig.ES_INDEX_ROLE') ] const userPrompt = `WARNING: Are you sure want to create the following elasticsearch indices: ${indices}?` diff --git a/scripts/es/deleteIndex.js b/scripts/es/deleteIndex.js index 6e30995a..724d3556 100644 --- a/scripts/es/deleteIndex.js +++ b/scripts/es/deleteIndex.js @@ -8,7 +8,8 @@ const helper = require('../../src/common/helper') const indices = [ config.get('esConfig.ES_INDEX_JOB'), config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + config.get('esConfig.ES_INDEX_ROLE') ] const userPrompt = `WARNING: this would remove existent data! Are you sure want to delete the following eleasticsearch indices: ${indices}?` diff --git a/scripts/es/reIndexAll.js b/scripts/es/reIndexAll.js index 802695dd..0367be11 100644 --- a/scripts/es/reIndexAll.js +++ b/scripts/es/reIndexAll.js @@ -34,6 +34,7 @@ async function indexAll () { await helper.indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) await helper.indexBulkDataToES(jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger) await helper.indexBulkDataToES(resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger) + await helper.indexBulkDataToES('Role', config.get('esConfig.ES_INDEX_ROLE'), logger) process.exit(0) } catch (err) { logger.logFullError(err, { component: 'indexAll' }) diff --git a/scripts/es/reIndexRoles.js b/scripts/es/reIndexRoles.js new file mode 100644 index 00000000..a4507aa9 --- /dev/null +++ b/scripts/es/reIndexRoles.js @@ -0,0 +1,37 @@ +/** + * Reindex Roles data in Elasticsearch using data from database + */ +const config = require('config') +const logger = require('../../src/common/logger') +const helper = require('../../src/common/helper') + +const roleId = helper.getParamFromCliArgs() +const index = config.get('esConfig.ES_INDEX_ROLE') +const reIndexAllRolesPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the index ${index}?` +const reIndexRolePrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the document with id ${roleId} in index ${index}?` + +async function reIndexRoles () { + if (roleId === null) { + await helper.promptUser(reIndexAllRolesPrompt, async () => { + try { + await helper.indexBulkDataToES('Role', index, logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'reIndexRoles' }) + process.exit(1) + } + }) + } else { + await helper.promptUser(reIndexRolePrompt, async () => { + try { + await helper.indexDataToEsById(roleId, 'Role', index, logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'reIndexRoles' }) + process.exit(1) + } + }) + } +} + +reIndexRoles() diff --git a/src/bootstrap.js b/src/bootstrap.js index 2999f131..896e6c9c 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -16,7 +16,7 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') -Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed') +Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed') Joi.title = () => Joi.string().max(128) Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) @@ -26,6 +26,7 @@ Joi.workPeriodPaymentStatus = () => Joi.string().valid('completed', 'cancelled') // See https://joi.dev/api/?v=17.3.0#string fro details why it's like this. // In many cases we would like to allow empty string to make it easier to create UI for editing data. Joi.stringAllowEmpty = () => Joi.string().allow('') +Joi.smallint = () => Joi.number().min(-32768).max(32767) function buildServices (dir) { const files = fs.readdirSync(dir) diff --git a/src/common/helper.js b/src/common/helper.js index 0ce11905..66cf32d1 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -2,50 +2,50 @@ * This file defines helper methods */ -const fs = require('fs'); -const querystring = require('querystring'); -const Confirm = require('prompt-confirm'); -const Bottleneck = require('bottleneck'); -const AWS = require('aws-sdk'); -const config = require('config'); -const HttpStatus = require('http-status-codes'); -const _ = require('lodash'); -const request = require('superagent'); -const elasticsearch = require('@elastic/elasticsearch'); +const fs = require('fs') +const querystring = require('querystring') +const Confirm = require('prompt-confirm') +const Bottleneck = require('bottleneck') +const AWS = require('aws-sdk') +const config = require('config') +const HttpStatus = require('http-status-codes') +const _ = require('lodash') +const request = require('superagent') +const elasticsearch = require('@elastic/elasticsearch') const { - ResponseError: ESResponseError, -} = require('@elastic/elasticsearch/lib/errors'); -const errors = require('../common/errors'); -const logger = require('./logger'); -const models = require('../models'); -const eventDispatcher = require('./eventDispatcher'); -const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper'); -const moment = require('moment'); + ResponseError: ESResponseError +} = require('@elastic/elasticsearch/lib/errors') +const errors = require('../common/errors') +const logger = require('./logger') +const models = require('../models') +const eventDispatcher = require('./eventDispatcher') +const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') +const moment = require('moment') const localLogger = { debug: (message) => logger.debug({ component: 'helper', context: message.context, - message: message.message, + message: message.message }), error: (message) => logger.error({ component: 'helper', context: message.context, - message: message.message, + message: message.message }), info: (message) => logger.info({ component: 'helper', context: message.context, - message: message.message, - }), -}; + message: message.message + }) +} -AWS.config.region = config.esConfig.AWS_REGION; +AWS.config.region = config.esConfig.AWS_REGION -const m2mAuth = require('tc-core-library-js').auth.m2m; +const m2mAuth = require('tc-core-library-js').auth.m2m const m2m = m2mAuth( _.pick(config, [ @@ -53,9 +53,9 @@ const m2m = m2mAuth( 'AUTH0_AUDIENCE', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', - 'AUTH0_PROXY_SERVER_URL', + 'AUTH0_PROXY_SERVER_URL' ]) -); +) const m2mForUbahn = m2mAuth({ AUTH0_AUDIENCE: config.AUTH0_AUDIENCE_UBAHN, @@ -64,20 +64,20 @@ const m2mForUbahn = m2mAuth({ 'TOKEN_CACHE_TIME', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', - 'AUTH0_PROXY_SERVER_URL', - ]), -}); + 'AUTH0_PROXY_SERVER_URL' + ]) +}) -let busApiClient; +let busApiClient /** * Get bus api client. * * @returns {Object} the bus api client */ -function getBusApiClient() { +function getBusApiClient () { if (busApiClient) { - return busApiClient; + return busApiClient } busApiClient = busApi( _.pick(config, [ @@ -88,17 +88,17 @@ function getBusApiClient() { 'AUTH0_CLIENT_SECRET', 'BUSAPI_URL', 'KAFKA_ERROR_TOPIC', - 'AUTH0_PROXY_SERVER_URL', + 'AUTH0_PROXY_SERVER_URL' ]) - ); - return busApiClient; + ) + return busApiClient } // ES Client mapping -const esClients = {}; +const esClients = {} // The es index property mapping -const esIndexPropertyMapping = {}; +const esIndexPropertyMapping = {} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { projectId: { type: 'integer' }, externalId: { type: 'keyword' }, @@ -113,11 +113,12 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { skills: { type: 'keyword' }, status: { type: 'keyword' }, isApplicationPageActive: { type: 'boolean' }, + roleIds: { type: 'keyword' }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { jobId: { type: 'keyword' }, userId: { type: 'keyword' }, @@ -150,14 +151,14 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, updatedBy: { type: 'keyword' }, - deletedAt: { type: 'date' }, - }, + deletedAt: { type: 'date' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { projectId: { type: 'integer' }, userId: { type: 'keyword' }, @@ -195,32 +196,59 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, - }, + updatedBy: { type: 'keyword' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, - }, + updatedBy: { type: 'keyword' } + } + }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, + updatedBy: { type: 'keyword' } +} +esIndexPropertyMapping[config.get('esConfig.ES_INDEX_ROLE')] = { + name: { type: 'keyword' }, + description: { type: 'keyword' }, + listOfSkills: { type: 'keyword' }, + rates: { + properties: { + global: { type: 'integer' }, + inCountry: { type: 'integer' }, + offShore: { type: 'integer' }, + rate30Global: { type: 'integer' }, + rate30InCountry: { type: 'integer' }, + rate30OffShore: { type: 'integer' }, + rate20Global: { type: 'integer' }, + rate20InCountry: { type: 'integer' }, + rate20OffShore: { type: 'integer' } + } }, + numberOfMembers: { type: 'integer' }, + numberOfMembersAvailable: { type: 'integer' }, + imageUrl: { type: 'keyword' }, + timeToCandidate: { type: 'integer' }, + timeToInterview: { type: 'integer' }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} /** * Get the first parameter from cli arguments */ -function getParamFromCliArgs() { - const filteredArgs = process.argv.filter((arg) => !arg.includes('--')); +function getParamFromCliArgs () { + const filteredArgs = process.argv.filter((arg) => !arg.includes('--')) if (filteredArgs.length > 2) { - return filteredArgs[2]; + return filteredArgs[2] } - return null; + return null } /** @@ -228,18 +256,18 @@ function getParamFromCliArgs() { * @param {string} promptQuery the query to ask the user * @param {function} cb the callback function */ -async function promptUser(promptQuery, cb) { +async function promptUser (promptQuery, cb) { if (process.argv.includes('--force')) { - await cb(); - return; + await cb() + return } - const prompt = new Confirm(promptQuery); + const prompt = new Confirm(promptQuery) prompt.ask(async (answer) => { if (answer) { - await cb(); + await cb() } - }); + }) } /** @@ -248,23 +276,23 @@ async function promptUser(promptQuery, cb) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function createIndex(index, logger, esClient = null) { +async function createIndex (index, logger, esClient = null) { if (!esClient) { - esClient = getESClient(); + esClient = getESClient() } await esClient.indices.create({ index, body: { mappings: { - properties: esIndexPropertyMapping[index], - }, - }, - }); + properties: esIndexPropertyMapping[index] + } + } + }) logger.info({ component: 'createIndex', - message: `ES Index ${index} creation succeeded!`, - }); + message: `ES Index ${index} creation succeeded!` + }) } /** @@ -273,45 +301,45 @@ async function createIndex(index, logger, esClient = null) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function deleteIndex(index, logger, esClient = null) { +async function deleteIndex (index, logger, esClient = null) { if (!esClient) { - esClient = getESClient(); + esClient = getESClient() } - await esClient.indices.delete({ index }); + await esClient.indices.delete({ index }) logger.info({ component: 'deleteIndex', - message: `ES Index ${index} deletion succeeded!`, - }); + message: `ES Index ${index} deletion succeeded!` + }) } /** * Split data into bulks * @param {Array} data the array of data to split */ -function getBulksFromDocuments(data) { - const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6; - const bulks = []; - let documentIndex = 0; - let currentBulkSize = 0; - let currentBulk = []; +function getBulksFromDocuments (data) { + const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6 + const bulks = [] + let documentIndex = 0 + let currentBulkSize = 0 + let currentBulk = [] while (true) { // break loop when parsed all documents if (documentIndex >= data.length) { - bulks.push(currentBulk); - break; + bulks.push(currentBulk) + break } // check if current document size is greater than the max bulk size, if so, throw error const currentDocumentSize = Buffer.byteLength( JSON.stringify(data[documentIndex]), 'utf-8' - ); + ) if (maxBytes < currentDocumentSize) { throw new Error( `Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.` - ); + ) } if ( @@ -320,17 +348,17 @@ function getBulksFromDocuments(data) { ) { // if adding the current document goes over the max bulk size OR goes over max number of docs // then push the current bulk to bulks array and reset the current bulk - bulks.push(currentBulk); - currentBulk = []; - currentBulkSize = 0; + bulks.push(currentBulk) + currentBulk = [] + currentBulkSize = 0 } else { // otherwise, add document to current bulk - currentBulk.push(data[documentIndex]); - currentBulkSize += currentDocumentSize; - documentIndex++; + currentBulk.push(data[documentIndex]) + currentBulkSize += currentDocumentSize + documentIndex++ } } - return bulks; + return bulks } /** @@ -339,57 +367,57 @@ function getBulksFromDocuments(data) { * @param {Object} indexName the index name * @param {Object} logger the logger object */ -async function indexBulkDataToES(modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); +async function indexBulkDataToES (modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) logger.info({ component: 'indexBulkDataToES', - message: `Reindexing of ${modelName}s started!`, - }); + message: `Reindexing of ${modelName}s started!` + }) - const esClient = getESClient(); + const esClient = getESClient() // clear index - const indexExistsRes = await esClient.indices.exists({ index: indexName }); + const indexExistsRes = await esClient.indices.exists({ index: indexName }) if (indexExistsRes.statusCode !== 404) { - await deleteIndex(indexName, logger, esClient); + await deleteIndex(indexName, logger, esClient) } - await createIndex(indexName, logger, esClient); + await createIndex(indexName, logger, esClient) // get data from db logger.info({ component: 'indexBulkDataToES', - message: 'Getting data from database', - }); - const model = models[modelName]; - const data = await model.findAll({ include }); - const rawObjects = _.map(data, (r) => r.toJSON()); + message: 'Getting data from database' + }) + const model = models[modelName] + const data = await model.findAll({ include }) + const rawObjects = _.map(data, (r) => r.toJSON()) if (_.isEmpty(rawObjects)) { logger.info({ component: 'indexBulkDataToES', - message: `No data in database for ${modelName}`, - }); - return; + message: `No data in database for ${modelName}` + }) + return } - const bulks = getBulksFromDocuments(rawObjects); + const bulks = getBulksFromDocuments(rawObjects) - const startTime = Date.now(); - let doneCount = 0; + const startTime = Date.now() + let doneCount = 0 for (const bulk of bulks) { // send bulk to esclient const body = bulk.flatMap((doc) => [ { index: { _index: indexName, _id: doc.id } }, - doc, - ]); - await esClient.bulk({ refresh: true, body }); - doneCount += bulk.length; + doc + ]) + await esClient.bulk({ refresh: true, body }) + doneCount += bulk.length // log metrics - const timeSpent = Date.now() - startTime; - const avgTimePerDocument = timeSpent / doneCount; - const estimatedLength = avgTimePerDocument * data.length; - const timeLeft = startTime + estimatedLength - Date.now(); + const timeSpent = Date.now() - startTime + const avgTimePerDocument = timeSpent / doneCount + const estimatedLength = avgTimePerDocument * data.length + const timeLeft = startTime + estimatedLength - Date.now() logger.info({ component: 'indexBulkDataToES', message: `Processed ${doneCount} of ${ @@ -398,8 +426,8 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { avgTimePerDocument )}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime( timeLeft - )}`, - }); + )}` + }) } } @@ -410,36 +438,36 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { * @param {string} id the job id * @param {Object} logger the logger object */ -async function indexDataToEsById(id, modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); +async function indexDataToEsById (id, modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) logger.info({ component: 'indexDataToEsById', - message: `Reindexing of ${modelName} with id ${id} started!`, - }); - const esClient = getESClient(); + message: `Reindexing of ${modelName} with id ${id} started!` + }) + const esClient = getESClient() logger.info({ component: 'indexDataToEsById', - message: 'Getting data from database', - }); - const model = models[modelName]; + message: 'Getting data from database' + }) + const model = models[modelName] - const data = await model.findById(id, include); + const data = await model.findById(id, include) logger.info({ component: 'indexDataToEsById', - message: 'Indexing data into Elasticsearch', - }); + message: 'Indexing data into Elasticsearch' + }) await esClient.index({ index: indexName, id: id, - body: data.dataValues, - }); + body: data.dataValues + }) logger.info({ component: 'indexDataToEsById', - message: 'Indexing complete!', - }); + message: 'Indexing complete!' + }) } /** @@ -448,68 +476,68 @@ async function indexDataToEsById(id, modelOpts, indexName, logger) { * @param {Array} dataModels the data models to import * @param {Object} logger the logger object */ -async function importData(pathToFile, dataModels, logger) { +async function importData (pathToFile, dataModels, logger) { // check if file exists if (!fs.existsSync(pathToFile)) { - throw new Error(`File with path ${pathToFile} does not exist`); + throw new Error(`File with path ${pathToFile} does not exist`) } // clear database - logger.info({ component: 'importData', message: 'Clearing database...' }); - await models.sequelize.sync({ force: true }); + logger.info({ component: 'importData', message: 'Clearing database...' }) + await models.sequelize.sync({ force: true }) - let transaction = null; - let currentModelName = null; + let transaction = null + let currentModelName = null try { // Start a transaction - transaction = await models.sequelize.transaction(); - const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()); + transaction = await models.sequelize.transaction() + const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()) for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index]; - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); + const modelOpts = dataModels[index] + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) - currentModelName = modelName; - const model = models[modelName]; - const modelRecords = jsonData[modelName]; + currentModelName = modelName + const model = models[modelName] + const modelRecords = jsonData[modelName] if (modelRecords && modelRecords.length > 0) { logger.info({ component: 'importData', - message: `Importing data for model: ${modelName}`, - }); + message: `Importing data for model: ${modelName}` + }) - await model.bulkCreate(modelRecords, { include, transaction }); + await model.bulkCreate(modelRecords, { include, transaction }) logger.info({ component: 'importData', - message: `Records imported for model: ${modelName} = ${modelRecords.length}`, - }); + message: `Records imported for model: ${modelName} = ${modelRecords.length}` + }) } else { logger.info({ component: 'importData', - message: `No records to import for model: ${modelName}`, - }); + message: `No records to import for model: ${modelName}` + }) } } // commit transaction only if all things went ok logger.info({ component: 'importData', - message: 'committing transaction to database...', - }); - await transaction.commit(); + message: 'committing transaction to database...' + }) + await transaction.commit() } catch (error) { logger.error({ component: 'importData', - message: `Error while writing data of model: ${currentModelName}`, - }); + message: `Error while writing data of model: ${currentModelName}` + }) // rollback all insert operations if (transaction) { logger.info({ component: 'importData', - message: 'rollback database transaction...', - }); - transaction.rollback(); + message: 'rollback database transaction...' + }) + transaction.rollback() } if (error.name && error.errors && error.fields) { // For sequelize validation errors, we throw only fields with data that helps in debugging error, @@ -519,11 +547,11 @@ async function importData(pathToFile, dataModels, logger) { modelName: currentModelName, name: error.name, errors: error.errors, - fields: error.fields, + fields: error.fields }) - ); + ) } else { - throw error; + throw error } } @@ -533,10 +561,10 @@ async function importData(pathToFile, dataModels, logger) { include: [ { model: models.Interview, - as: 'interviews', - }, - ], - }; + as: 'interviews' + } + ] + } const resourceBookingModelOpts = { modelName: 'ResourceBooking', include: [ @@ -546,23 +574,24 @@ async function importData(pathToFile, dataModels, logger) { include: [ { model: models.WorkPeriodPayment, - as: 'payments', - }, - ], - }, - ], - }; - await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger); + as: 'payments' + } + ] + } + ] + } + await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) await indexBulkDataToES( jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger - ); + ) await indexBulkDataToES( resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger - ); + ) + await indexBulkDataToES('Role', config.get('esConfig.ES_INDEX_ROLE'), logger) } /** @@ -571,74 +600,74 @@ async function importData(pathToFile, dataModels, logger) { * @param {Array} dataModels the data models to export * @param {Object} logger the logger object */ -async function exportData(pathToFile, dataModels, logger) { +async function exportData (pathToFile, dataModels, logger) { logger.info({ component: 'exportData', - message: `Start Saving data to file with path ${pathToFile}....`, - }); + message: `Start Saving data to file with path ${pathToFile}....` + }) - const allModelsRecords = {}; + const allModelsRecords = {} for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index]; - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); - const modelRecords = await models[modelName].findAll({ include }); - const rawRecords = _.map(modelRecords, (r) => r.toJSON()); - allModelsRecords[modelName] = rawRecords; + const modelOpts = dataModels[index] + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) + const modelRecords = await models[modelName].findAll({ include }) + const rawRecords = _.map(modelRecords, (r) => r.toJSON()) + allModelsRecords[modelName] = rawRecords logger.info({ component: 'exportData', - message: `Records loaded for model: ${modelName} = ${rawRecords.length}`, - }); + message: `Records loaded for model: ${modelName} = ${rawRecords.length}` + }) } - fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)); + fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)) logger.info({ component: 'exportData', - message: 'End Saving data to file....', - }); + message: 'End Saving data to file....' + }) } /** * Format a time in milliseconds into a human readable format * @param {Date} milliseconds the number of milliseconds */ -function formatTime(millisec) { - const ms = Math.floor(millisec % 1000); - const secs = Math.floor((millisec / 1000) % 60); - const mins = Math.floor((millisec / (1000 * 60)) % 60); - const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24); - const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7); - const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4); - const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12); - const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)); - - let formattedTime = '0 milliseconds'; +function formatTime (millisec) { + const ms = Math.floor(millisec % 1000) + const secs = Math.floor((millisec / 1000) % 60) + const mins = Math.floor((millisec / (1000 * 60)) % 60) + const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24) + const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7) + const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4) + const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12) + const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)) + + let formattedTime = '0 milliseconds' if (ms > 0) { - formattedTime = `${ms} milliseconds`; + formattedTime = `${ms} milliseconds` } if (secs > 0) { - formattedTime = `${secs} seconds ${formattedTime}`; + formattedTime = `${secs} seconds ${formattedTime}` } if (mins > 0) { - formattedTime = `${mins} minutes ${formattedTime}`; + formattedTime = `${mins} minutes ${formattedTime}` } if (hrs > 0) { - formattedTime = `${hrs} hours ${formattedTime}`; + formattedTime = `${hrs} hours ${formattedTime}` } if (days > 0) { - formattedTime = `${days} days ${formattedTime}`; + formattedTime = `${days} days ${formattedTime}` } if (weeks > 0) { - formattedTime = `${weeks} weeks ${formattedTime}`; + formattedTime = `${weeks} weeks ${formattedTime}` } if (mnths > 0) { - formattedTime = `${mnths} months ${formattedTime}`; + formattedTime = `${mnths} months ${formattedTime}` } if (yrs > 0) { - formattedTime = `${yrs} years ${formattedTime}`; + formattedTime = `${yrs} years ${formattedTime}` } - return formattedTime.trim(); + return formattedTime.trim() } /** @@ -647,30 +676,30 @@ function formatTime(millisec) { * @param {Array} source the array in which to search for the term * @param {Array | String} term the term to search */ -function checkIfExists(source, term) { - let terms; +function checkIfExists (source, term) { + let terms if (!_.isArray(source)) { - throw new Error('Source argument should be an array'); + throw new Error('Source argument should be an array') } - source = source.map((s) => s.toLowerCase()); + source = source.map((s) => s.toLowerCase()) if (_.isString(term)) { - terms = term.toLowerCase().split(' '); + terms = term.toLowerCase().split(' ') } else if (_.isArray(term)) { - terms = term.map((t) => t.toLowerCase()); + terms = term.map((t) => t.toLowerCase()) } else { - throw new Error('Term argument should be either a string or an array'); + throw new Error('Term argument should be either a string or an array') } for (let i = 0; i < terms.length; i++) { if (source.includes(terms[i])) { - return true; + return true } } - return false; + return false } /** @@ -678,10 +707,10 @@ function checkIfExists(source, term) { * @param {Function} fn the async function * @returns {Function} the wrapped function */ -function wrapExpress(fn) { +function wrapExpress (fn) { return function (req, res, next) { - fn(req, res, next).catch(next); - }; + fn(req, res, next).catch(next) + } } /** @@ -689,20 +718,20 @@ function wrapExpress(fn) { * @param obj the object (controller exports) * @returns {Object|Array} the wrapped object */ -function autoWrapExpress(obj) { +function autoWrapExpress (obj) { if (_.isArray(obj)) { - return obj.map(autoWrapExpress); + return obj.map(autoWrapExpress) } if (_.isFunction(obj)) { if (obj.constructor.name === 'AsyncFunction') { - return wrapExpress(obj); + return wrapExpress(obj) } - return obj; + return obj } _.each(obj, (value, key) => { - obj[key] = autoWrapExpress(value); - }); - return obj; + obj[key] = autoWrapExpress(value) + }) + return obj } /** @@ -711,11 +740,11 @@ function autoWrapExpress(obj) { * @param {Number} page the page number * @returns {String} link for the page */ -function getPageLink(req, page) { - const q = _.assignIn({}, req.query, { page }); +function getPageLink (req, page) { + const q = _.assignIn({}, req.query, { page }) return `${req.protocol}://${req.get('Host')}${req.baseUrl}${ req.path - }?${querystring.stringify(q)}`; + }?${querystring.stringify(q)}` } /** @@ -724,31 +753,31 @@ function getPageLink(req, page) { * @param {Object} res the HTTP response * @param {Object} result the operation result */ -function setResHeaders(req, res, result) { - const totalPages = Math.ceil(result.total / result.perPage); +function setResHeaders (req, res, result) { + const totalPages = Math.ceil(result.total / result.perPage) if (result.page > 1) { - res.set('X-Prev-Page', result.page - 1); + res.set('X-Prev-Page', result.page - 1) } if (result.page < totalPages) { - res.set('X-Next-Page', result.page + 1); + res.set('X-Next-Page', result.page + 1) } - res.set('X-Page', result.page); - res.set('X-Per-Page', result.perPage); - res.set('X-Total', result.total); - res.set('X-Total-Pages', totalPages); + res.set('X-Page', result.page) + res.set('X-Per-Page', result.perPage) + res.set('X-Total', result.total) + res.set('X-Total-Pages', totalPages) // set Link header if (totalPages > 0) { let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink( req, totalPages - )}>; rel="last"`; + )}>; rel="last"` if (result.page > 1) { - link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"`; + link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"` } if (result.page < totalPages) { - link += `, <${getPageLink(req, result.page + 1)}>; rel="next"`; + link += `, <${getPageLink(req, result.page + 1)}>; rel="next"` } - res.set('Link', link); + res.set('Link', link) } } @@ -756,30 +785,30 @@ function setResHeaders(req, res, result) { * Get ES Client * @return {Object} Elastic Host Client Instance */ -function getESClient() { +function getESClient () { if (esClients.client) { - return esClients.client; + return esClients.client } - const host = config.esConfig.HOST; - const cloudId = config.esConfig.ELASTICCLOUD.id; + const host = config.esConfig.HOST + const cloudId = config.esConfig.ELASTICCLOUD.id if (cloudId) { // Elastic Cloud configuration esClients.client = new elasticsearch.Client({ cloud: { - id: cloudId, + id: cloudId }, auth: { username: config.esConfig.ELASTICCLOUD.username, - password: config.esConfig.ELASTICCLOUD.password, - }, - }); + password: config.esConfig.ELASTICCLOUD.password + } + }) } else { esClients.client = new elasticsearch.Client({ - node: host, - }); + node: host + }) } - return esClients.client; + return esClients.client } /* @@ -790,8 +819,8 @@ const getM2MToken = async () => { return await m2m.getMachineToken( config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET - ); -}; + ) +} /* * Function to get M2M token for U-Bahn @@ -801,8 +830,8 @@ const getM2MUbahnToken = async () => { return await m2mForUbahn.getMachineToken( config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET - ); -}; + ) +} /** * Function to encode query string @@ -810,17 +839,17 @@ const getM2MUbahnToken = async () => { * @param {String} nesting the nesting string * @returns {String} query string */ -function encodeQueryString(queryObj, nesting = '') { +function encodeQueryString (queryObj, nesting = '') { const pairs = Object.entries(queryObj).map(([key, val]) => { // Handle the nested, recursive case, where the value to encode is an object itself if (typeof val === 'object') { - return encodeQueryString(val, nesting + `${key}.`); + return encodeQueryString(val, nesting + `${key}.`) } else { // Handle base case, where the value to encode is simply a string. - return [nesting + key, val].map(querystring.escape).join('='); + return [nesting + key, val].map(querystring.escape).join('=') } - }); - return pairs.join('&'); + }) + return pairs.join('&') } /** @@ -828,31 +857,31 @@ function encodeQueryString(queryObj, nesting = '') { * @param {Integer} externalId the legacy user id * @returns {Array} the users found */ -async function listUsersByExternalId(externalId) { +async function listUsersByExternalId (externalId) { // return empty list if externalId is null or undefined if (!!externalId !== true) { - return []; + return [] } - const token = await getM2MUbahnToken(); + const token = await getM2MUbahnToken() const q = { enrich: true, externalProfile: { organizationId: config.ORG_ID, - externalId, - }, - }; - const url = `${config.TC_API}/users?${encodeQueryString(q)}`; + externalId + } + } + const url = `${config.TC_API}/users?${encodeQueryString(q)}` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listUserByExternalId', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -860,14 +889,14 @@ async function listUsersByExternalId(externalId) { * @param {Integer} externalId the legacy user id * @returns {Object} the user */ -async function getUserByExternalId(externalId) { - const users = await listUsersByExternalId(externalId); +async function getUserByExternalId (externalId) { + const users = await listUsersByExternalId(externalId) if (_.isEmpty(users)) { throw new errors.NotFoundError( `externalId: ${externalId} "user" not found` - ); + ) } - return users[0]; + return users[0] } /** @@ -876,24 +905,24 @@ async function getUserByExternalId(externalId) { * @params {Object} payload the payload * @params {Object} options the extra options to control the function */ -async function postEvent(topic, payload, options = {}) { +async function postEvent (topic, payload, options = {}) { logger.debug({ component: 'helper', context: 'postEvent', message: `Posting event to Kafka topic ${topic}, ${JSON.stringify( payload - )}`, - }); - const client = getBusApiClient(); + )}` + }) + const client = getBusApiClient() const message = { topic, originator: config.KAFKA_MESSAGE_ORIGINATOR, timestamp: new Date().toISOString(), 'mime-type': 'application/json', - payload, - }; - await client.postEvent(message); - await eventDispatcher.handleEvent(topic, { value: payload, options }); + payload + } + await client.postEvent(message) + await eventDispatcher.handleEvent(topic, { value: payload, options }) } /** @@ -902,11 +931,11 @@ async function postEvent(topic, payload, options = {}) { * @param {Object} err the err * @returns {Boolean} the result */ -function isDocumentMissingException(err) { +function isDocumentMissingException (err) { if (err.statusCode === 404 && err instanceof ESResponseError) { - return true; + return true } - return false; + return false } /** @@ -915,34 +944,34 @@ function isDocumentMissingException(err) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getProjects(currentUser, criteria = {}) { - let token; +async function getProjects (currentUser, criteria = {}) { + let token if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken(); - token = `Bearer ${m2mToken}`; + const m2mToken = await getM2MToken() + token = `Bearer ${m2mToken}` } else { - token = currentUser.jwtToken; + token = currentUser.jwtToken } - const url = `${config.TC_API}/projects?type=talent-as-a-service`; + const url = `${config.TC_API}/projects?type=talent-as-a-service` const res = await request .get(url) .query(criteria) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getProjects', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) const result = _.map(res.body, (item) => { - return _.pick(item, ['id', 'name', 'invites', 'members']); - }); + return _.pick(item, ['id', 'name', 'invites', 'members']) + }) return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result, - }; + result + } } /** @@ -951,24 +980,24 @@ async function getProjects(currentUser, criteria = {}) { * @param {String} userId the legacy user id * @returns {Object} the user */ -async function getTopcoderUserById(userId) { - const token = await getM2MToken(); +async function getTopcoderUserById (userId) { + const token = await getM2MToken() const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `id=${userId}` }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getTopcoderUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - const user = _.get(res.body, 'result.content[0]'); + message: `response body: ${JSON.stringify(res.body)}` + }) + const user = _.get(res.body, 'result.content[0]') if (!user) { throw new errors.NotFoundError( `userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}` - ); + ) } - return user; + return user } /** @@ -976,31 +1005,31 @@ async function getTopcoderUserById(userId) { * @param {String} userId the user id * @returns the request result */ -async function getUserById(userId, enrich) { - const token = await getM2MUbahnToken(); +async function getUserById (userId, enrich) { + const token = await getM2MUbahnToken() const res = await request .get(`${config.TC_API}/users/${userId}` + (enrich ? '?enrich=true' : '')) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) - const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']); + const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']) if (enrich) { user.skills = (res.body.skills || []).map((skillObj) => _.pick(skillObj.skill, ['id', 'name']) - ); - const attributes = _.get(res, 'body.attributes', []); + ) + const attributes = _.get(res, 'body.attributes', []) user.attributes = _.map(attributes, (attr) => _.pick(attr, ['id', 'value', 'attribute.id', 'attribute.name']) - ); + ) } - return user; + return user } /** @@ -1008,19 +1037,19 @@ async function getUserById(userId, enrich) { * @param {Object} data the user data * @returns the request result */ -async function createUbahnUser({ handle, firstName, lastName }) { - const token = await getM2MUbahnToken(); +async function createUbahnUser ({ handle, firstName, lastName }) { + const token = await getM2MUbahnToken() const res = await request .post(`${config.TC_API}/users`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ handle, firstName, lastName }); + .send({ handle, firstName, lastName }) localLogger.debug({ context: 'createUbahnUser', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id']) } /** @@ -1028,21 +1057,21 @@ async function createUbahnUser({ handle, firstName, lastName }) { * @param {String} userId the user id(with uuid format) * @param {Object} data the profile data */ -async function createUserExternalProfile( +async function createUserExternalProfile ( userId, { organizationId, externalId } ) { - const token = await getM2MUbahnToken(); + const token = await getM2MUbahnToken() const res = await request .post(`${config.TC_API}/users/${userId}/externalProfiles`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ organizationId, externalId: String(externalId) }); + .send({ organizationId, externalId: String(externalId) }) localLogger.debug({ context: 'createUserExternalProfile', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) } /** @@ -1050,23 +1079,23 @@ async function createUserExternalProfile( * @param {Array} handles the handle array * @returns the request result */ -async function getMembers(handles) { - const token = await getM2MToken(); +async function getMembers (handles) { + const token = await getM2MToken() const handlesStr = _.map(handles, (handle) => { - return '%22' + handle.toLowerCase() + '%22'; - }).join(','); - const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]`; + return '%22' + handle.toLowerCase() + '%22' + }).join(',') + const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getMembers', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -1075,36 +1104,36 @@ async function getMembers(handles) { * @param {Number} id project id * @returns the request result */ -async function getProjectById(currentUser, id) { - let token; +async function getProjectById (currentUser, id) { + let token if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken(); - token = `Bearer ${m2mToken}`; + const m2mToken = await getM2MToken() + token = `Bearer ${m2mToken}` } else { - token = currentUser.jwtToken; + token = currentUser.jwtToken } - const url = `${config.TC_API}/projects/${id}`; + const url = `${config.TC_API}/projects/${id}` try { const res = await request .get(url) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getProjectById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id', 'name', 'invites', 'members']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id', 'name', 'invites', 'members']) } catch (err) { if (err.status === HttpStatus.FORBIDDEN) { throw new errors.ForbiddenError( `You are not allowed to access the project with id ${id}` - ); + ) } if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${id} project not found`); + throw new errors.NotFoundError(`id: ${id} project not found`) } - throw err; + throw err } } @@ -1115,33 +1144,33 @@ async function getProjectById(currentUser, id) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getTopcoderSkills(criteria) { - const token = await getM2MUbahnToken(); +async function getTopcoderSkills (criteria) { + const token = await getM2MUbahnToken() try { const res = await request .get(`${config.TC_API}/skills`) .query({ skillProviderId: config.TOPCODER_SKILL_PROVIDER_ID, - ...criteria, + ...criteria }) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getTopcoderSkills', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result: res.body, - }; + result: res.body + } } catch (err) { if (err.status === HttpStatus.BAD_REQUEST) { - throw new errors.BadRequestError(err.response.body.message); + throw new errors.BadRequestError(err.response.body.message) } - throw err; + throw err } } @@ -1150,18 +1179,18 @@ async function getTopcoderSkills(criteria) { * @param {String} skillId the skill Id * @returns the request result */ -async function getSkillById(skillId) { - const token = await getM2MUbahnToken(); +async function getSkillById (skillId) { + const token = await getM2MUbahnToken() const res = await request .get(`${config.TC_API}/skills/${skillId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getSkillById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id', 'name']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id', 'name']) } /** @@ -1174,22 +1203,22 @@ async function getSkillById(skillId) { * @params {Object} currentUser the user who perform this operation * @returns {String} the ubahn user id */ -async function ensureUbahnUserId(currentUser) { +async function ensureUbahnUserId (currentUser) { try { - return (await getUserByExternalId(currentUser.userId)).id; + return (await getUserByExternalId(currentUser.userId)).id } catch (err) { if (!(err instanceof errors.NotFoundError)) { - throw err; + throw err } - const topcoderUser = await getTopcoderUserById(currentUser.userId); + const topcoderUser = await getTopcoderUserById(currentUser.userId) const user = await createUbahnUser( _.pick(topcoderUser, ['handle', 'firstName', 'lastName']) - ); + ) await createUserExternalProfile(user.id, { organizationId: config.ORG_ID, - externalId: currentUser.userId, - }); - return user.id; + externalId: currentUser.userId + }) + return user.id } } @@ -1199,8 +1228,8 @@ async function ensureUbahnUserId(currentUser) { * @param {String} jobId the job id * @returns {Object} the job data */ -async function ensureJobById(jobId) { - return models.Job.findById(jobId); +async function ensureJobById (jobId) { + return models.Job.findById(jobId) } /** @@ -1209,8 +1238,8 @@ async function ensureJobById(jobId) { * @param {String} resourceBookingId the resourceBooking id * @returns {Object} the resourceBooking data */ -async function ensureResourceBookingById(resourceBookingId) { - return models.ResourceBooking.findById(resourceBookingId); +async function ensureResourceBookingById (resourceBookingId) { + return models.ResourceBooking.findById(resourceBookingId) } /** @@ -1218,8 +1247,8 @@ async function ensureResourceBookingById(resourceBookingId) { * @param {String} workPeriodId the workPeriod id * @returns the workPeriod data */ -async function ensureWorkPeriodById(workPeriodId) { - return models.WorkPeriod.findById(workPeriodId); +async function ensureWorkPeriodById (workPeriodId) { + return models.WorkPeriod.findById(workPeriodId) } /** @@ -1228,24 +1257,24 @@ async function ensureWorkPeriodById(workPeriodId) { * @param {String} jobId the user id * @returns {Object} the user data */ -async function ensureUserById(userId) { - const token = await getM2MUbahnToken(); +async function ensureUserById (userId) { + const token = await getM2MUbahnToken() try { const res = await request .get(`${config.TC_API}/users/${userId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'ensureUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${userId} "user" not found`); + throw new errors.NotFoundError(`id: ${userId} "user" not found`) } - throw err; + throw err } } @@ -1254,12 +1283,12 @@ async function ensureUserById(userId) { * * @returns {Object} the M2M auth user */ -function getAuditM2Muser() { +function getAuditM2Muser () { return { isMachine: true, userId: config.m2m.M2M_AUDIT_USER_ID, - handle: config.m2m.M2M_AUDIT_HANDLE, - }; + handle: config.m2m.M2M_AUDIT_HANDLE + } } /** @@ -1271,24 +1300,24 @@ function getAuditM2Muser() { * @param {Number} projectId project id * @returns the result */ -async function checkIsMemberOfProject(userId, projectId) { - const m2mToken = await getM2MToken(); +async function checkIsMemberOfProject (userId, projectId) { + const m2mToken = await getM2MToken() const res = await request .get(`${config.TC_API}/projects/${projectId}`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const memberIdList = _.map(res.body.members, 'userId'); + .set('Accept', 'application/json') + const memberIdList = _.map(res.body.members, 'userId') localLogger.debug({ context: 'checkIsMemberOfProject', message: `the members of project ${projectId}: ${JSON.stringify( memberIdList - )}, authUserId: ${JSON.stringify(userId)}`, - }); + )}, authUserId: ${JSON.stringify(userId)}` + }) if (!memberIdList.includes(userId)) { throw new errors.UnauthorizedError( `userId: ${userId} the user is not a member of project ${projectId}` - ); + ) } } @@ -1298,11 +1327,11 @@ async function checkIsMemberOfProject(userId, projectId) { * @param {Array} handles the array of handles * @returns {Array} the member details */ -async function getMemberDetailsByHandles(handles) { +async function getMemberDetailsByHandles (handles) { if (!handles.length) { - return []; + return [] } - const token = await getM2MToken(); + const token = await getM2MToken() const res = await request .get(`${config.TOPCODER_MEMBERS_API}/_search`) .query({ @@ -1310,15 +1339,15 @@ async function getMemberDetailsByHandles(handles) { handles, (handle) => `handleLower:${handle.toLowerCase()}` ).join(' OR '), - fields: 'userId,handle,firstName,lastName,email', + fields: 'userId,handle,firstName,lastName,email' }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getMemberDetailsByHandles', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res.body, 'result.content'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res.body, 'result.content') } /** @@ -1327,17 +1356,17 @@ async function getMemberDetailsByHandles(handles) { * @param {String} handle the user handle * @returns {Object} the member details */ -async function getV3MemberDetailsByHandle(handle) { - const token = await getM2MToken(); +async function getV3MemberDetailsByHandle (handle) { + const token = await getM2MToken() const res = await request .get(`${config.TOPCODER_MEMBERS_API}/${handle}`) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getV3MemberDetailsByHandle', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res.body, 'result.content'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res.body, 'result.content') } /** @@ -1347,20 +1376,20 @@ async function getV3MemberDetailsByHandle(handle) { * @param {String} email the email * @returns {Array} the member details */ -async function _getMemberDetailsByEmail(token, email) { +async function _getMemberDetailsByEmail (token, email) { const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `email=${email}`, - fields: 'handle,id,email,firstName,lastName', + fields: 'handle,id,email,firstName,lastName' }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: '_getMemberDetailsByEmail', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res.body, 'result.content'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res.body, 'result.content') } /** @@ -1370,25 +1399,25 @@ async function _getMemberDetailsByEmail(token, email) { * @param {Array} emails the array of emails * @returns {Array} the member details */ -async function getMemberDetailsByEmails(emails) { - const token = await getM2MToken(); +async function getMemberDetailsByEmails (emails) { + const token = await getM2MToken() const limiter = new Bottleneck({ - maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API, - }); + maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API + }) const membersArray = await Promise.all( emails.map((email) => limiter.schedule(() => _getMemberDetailsByEmail(token, email).catch((error) => { localLogger.error({ context: 'getMemberDetailsByEmails', - message: error.message, - }); - return []; + message: error.message + }) + return [] }) ) ) - ); - return _.flatten(membersArray); + ) + return _.flatten(membersArray) } /** @@ -1399,20 +1428,20 @@ async function getMemberDetailsByEmails(emails) { * @param {Object} criteria the filtering criteria * @returns {Object} the member created */ -async function createProjectMember(projectId, data, criteria) { - const m2mToken = await getM2MToken(); +async function createProjectMember (projectId, data, criteria) { + const m2mToken = await getM2MToken() const { body: member } = await request .post(`${config.TC_API}/projects/${projectId}/members`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') .query(criteria) - .send(data); + .send(data) localLogger.debug({ context: 'createProjectMember', - message: `response body: ${JSON.stringify(member)}`, - }); - return member; + message: `response body: ${JSON.stringify(member)}` + }) + return member } /** @@ -1422,21 +1451,21 @@ async function createProjectMember(projectId, data, criteria) { * @param {Object} criteria the search criteria * @returns {Array} the project members */ -async function listProjectMembers(currentUser, projectId, criteria = {}) { +async function listProjectMembers (currentUser, projectId, criteria = {}) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken const { body: members } = await request .get(`${config.TC_API}/projects/${projectId}/members`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listProjectMembers', - message: `response body: ${JSON.stringify(members)}`, - }); - return members; + message: `response body: ${JSON.stringify(members)}` + }) + return members } /** @@ -1446,21 +1475,21 @@ async function listProjectMembers(currentUser, projectId, criteria = {}) { * @param {Object} criteria the search criteria * @returns {Array} the member invites */ -async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { +async function listProjectMemberInvites (currentUser, projectId, criteria = {}) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken const { body: invites } = await request .get(`${config.TC_API}/projects/${projectId}/invites`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listProjectMemberInvites', - message: `response body: ${JSON.stringify(invites)}`, - }); - return invites; + message: `response body: ${JSON.stringify(invites)}` + }) + return invites } /** @@ -1470,24 +1499,24 @@ async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { * @param {String} projectMemberId the id of the project member * @returns {undefined} */ -async function deleteProjectMember(currentUser, projectId, projectMemberId) { +async function deleteProjectMember (currentUser, projectId, projectMemberId) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken try { await request .delete( `${config.TC_API}/projects/${projectId}/members/${projectMemberId}` ) - .set('Authorization', token); + .set('Authorization', token) } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { throw new errors.NotFoundError( `projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}` - ); + ) } - throw err; + throw err } } @@ -1497,13 +1526,13 @@ async function deleteProjectMember(currentUser, projectId, projectMemberId) { * @param {String} attributeName Requested attribute name, e.g. "email" * @returns attribute value */ -function getUserAttributeValue(user, attributeName) { - const attributes = _.get(user, 'attributes', []); +function getUserAttributeValue (user, attributeName) { + const attributes = _.get(user, 'attributes', []) const targetAttribute = _.find( attributes, (a) => a.attribute.name === attributeName - ); - return _.get(targetAttribute, 'value'); + ) + return _.get(targetAttribute, 'value') } /** @@ -1513,34 +1542,34 @@ function getUserAttributeValue(user, attributeName) { * @param {String} token m2m token * @returns {Object} the challenge created */ -async function createChallenge(data, token) { +async function createChallenge (data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/challenges`; + const url = `${config.TC_API}/challenges` localLogger.debug({ context: 'createChallenge', - message: `EndPoint: POST ${url}`, - }); + message: `EndPoint: POST ${url}` + }) localLogger.debug({ context: 'createChallenge', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: challenge, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createChallenge', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'createChallenge', - message: `Response Body: ${JSON.stringify(challenge)}`, - }); - return challenge; + message: `Response Body: ${JSON.stringify(challenge)}` + }) + return challenge } /** @@ -1551,34 +1580,34 @@ async function createChallenge(data, token) { * @param {String} token m2m token * @returns {Object} the challenge updated */ -async function updateChallenge(challengeId, data, token) { +async function updateChallenge (challengeId, data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/challenges/${challengeId}`; + const url = `${config.TC_API}/challenges/${challengeId}` localLogger.debug({ context: 'updateChallenge', - message: `EndPoint: PATCH ${url}`, - }); + message: `EndPoint: PATCH ${url}` + }) localLogger.debug({ context: 'updateChallenge', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: challenge, status: httpStatus } = await request .patch(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'updateChallenge', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'updateChallenge', - message: `Response Body: ${JSON.stringify(challenge)}`, - }); - return challenge; + message: `Response Body: ${JSON.stringify(challenge)}` + }) + return challenge } /** @@ -1588,34 +1617,34 @@ async function updateChallenge(challengeId, data, token) { * @param {String} token m2m token * @returns {Object} the resource created */ -async function createChallengeResource(data, token) { +async function createChallengeResource (data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/resources`; + const url = `${config.TC_API}/resources` localLogger.debug({ context: 'createChallengeResource', - message: `EndPoint: POST ${url}`, - }); + message: `EndPoint: POST ${url}` + }) localLogger.debug({ context: 'createChallengeResource', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: resource, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createChallengeResource', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'createChallengeResource', - message: `Response Body: ${JSON.stringify(resource)}`, - }); - return resource; + message: `Response Body: ${JSON.stringify(resource)}` + }) + return resource } /** @@ -1624,40 +1653,40 @@ async function createChallengeResource(data, token) { * @param {Date} end end date of the resource booking * @returns {Array<{startDate:Date, endDate:Date, daysWorked:number}>} information about workPeriods */ -function extractWorkPeriods(start, end) { +function extractWorkPeriods (start, end) { // calculate maximum possible daysWorked for a week - function getDaysWorked(week) { + function getDaysWorked (week) { if (weeks === 1) { - return Math.min(endDay, 5) - Math.max(startDay, 1) + 1; + return Math.min(endDay, 5) - Math.max(startDay, 1) + 1 } else if (week === 0) { - return Math.min(6 - startDay, 5); + return Math.min(6 - startDay, 5) } else if (week === weeks - 1) { - return Math.min(endDay, 5); - } else return 5; + return Math.min(endDay, 5) + } else return 5 } - const periods = []; + const periods = [] if (_.isNil(start) || _.isNil(end)) { - return periods; + return periods } - const startDate = moment(start); - const startDay = startDate.get('day'); - startDate.set('day', 0).startOf('day'); + const startDate = moment(start) + const startDay = startDate.get('day') + startDate.set('day', 0).startOf('day') - const endDate = moment(end); - const endDay = endDate.get('day'); - endDate.set('day', 6).endOf('day'); + const endDate = moment(end) + const endDay = endDate.get('day') + endDate.set('day', 6).endOf('day') - const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7; + const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7 for (let i = 0; i < weeks; i++) { periods.push({ startDate: startDate.format('YYYY-MM-DD'), endDate: startDate.add(6, 'day').format('YYYY-MM-DD'), - daysWorked: getDaysWorked(i), - }); - startDate.add(1, 'day'); + daysWorked: getDaysWorked(i) + }) + startDate.add(1, 'day') } - return periods; + return periods } /** @@ -1666,19 +1695,19 @@ function extractWorkPeriods(start, end) { * @param {String} userHandle user handle * @returns {String} email address of the user */ -async function getUserByHandle(userHandle) { - const token = await getM2MToken(); - const url = `${config.TC_API}/members/${userHandle}`; +async function getUserByHandle (userHandle) { + const token = await getM2MToken() + const url = `${config.TC_API}/members/${userHandle}` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getUserByHandle', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res, 'body'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res, 'body') } /** @@ -1687,14 +1716,14 @@ async function getUserByHandle(userHandle) { * @param {*} object of json that would be replaced in string * @returns */ -async function substituteStringByObject(string, object) { +async function substituteStringByObject (string, object) { for (var key in object) { if (!Object.prototype.hasOwnProperty.call(object, key)) { - continue; + continue } - string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]); + string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]) } - return string; + return string } /** @@ -1702,19 +1731,19 @@ async function substituteStringByObject(string, object) { * @param {Object} data title of project and any other info * @returns {Object} the project created */ -async function createProject(currentUser, data) { - const token = currentUser.jwtToken; +async function createProject (currentUser, data) { + const token = currentUser.jwtToken const res = await request .post(`${config.TC_API}/projects/`) .set('Authorization', token) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createProject', - message: `response body: ${JSON.stringify(res)}`, - }); - return _.get(res, 'body'); + message: `response body: ${JSON.stringify(res)}` + }) + return _.get(res, 'body') } module.exports = { @@ -1733,9 +1762,9 @@ module.exports = { getUserId: async (userId) => { // check m2m user id if (userId === config.m2m.M2M_AUDIT_USER_ID) { - return config.m2m.M2M_AUDIT_USER_ID; + return config.m2m.M2M_AUDIT_USER_ID } - return ensureUbahnUserId({ userId }); + return ensureUbahnUserId({ userId }) }, getUserByExternalId, getM2MToken, @@ -1769,5 +1798,5 @@ module.exports = { extractWorkPeriods, getUserByHandle, substituteStringByObject, - createProject, -}; + createProject +} diff --git a/src/controllers/RoleController.js b/src/controllers/RoleController.js new file mode 100644 index 00000000..747cbe4d --- /dev/null +++ b/src/controllers/RoleController.js @@ -0,0 +1,59 @@ +/** + * Controller for Role endpoints + */ +const HttpStatus = require('http-status-codes') +const service = require('../services/RoleService') + +/** + * Get role by id + * @param req the request + * @param res the response + */ +async function getRole (req, res) { + res.send(await service.getRole(req.authUser, req.params.id, req.query.fromDb)) +} + +/** + * Create role + * @param req the request + * @param res the response + */ +async function createRole (req, res) { + res.send(await service.createRole(req.authUser, req.body)) +} + +/** + * update role by id + * @param req the request + * @param res the response + */ +async function updateRole (req, res) { + res.send(await service.updateRole(req.authUser, req.params.id, req.body)) +} + +/** + * Delete role by id + * @param req the request + * @param res the response + */ +async function deleteRole (req, res) { + await service.deleteRole(req.authUser, req.params.id) + res.status(HttpStatus.NO_CONTENT).end() +} + +/** + * Search roles + * @param req the request + * @param res the response + */ +async function searchRoles (req, res) { + res.send(await service.searchRoles(req.authUser, req.query)) +} + +module.exports = { + getRole, + createRole, + updateRole, + deleteRole, + searchRoles +} diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index ca4f1bca..26d70738 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -1,19 +1,19 @@ /** * Controller for TaaS teams endpoints */ -const HttpStatus = require('http-status-codes'); -const service = require('../services/TeamService'); -const helper = require('../common/helper'); +const HttpStatus = require('http-status-codes') +const service = require('../services/TeamService') +const helper = require('../common/helper') /** * Search teams * @param req the request * @param res the response */ -async function searchTeams(req, res) { - const result = await service.searchTeams(req.authUser, req.query); - helper.setResHeaders(req, res, result); - res.send(result.result); +async function searchTeams (req, res) { + const result = await service.searchTeams(req.authUser, req.query) + helper.setResHeaders(req, res, result) + res.send(result.result) } /** @@ -21,8 +21,8 @@ async function searchTeams(req, res) { * @param req the request * @param res the response */ -async function getTeam(req, res) { - res.send(await service.getTeam(req.authUser, req.params.id)); +async function getTeam (req, res) { + res.send(await service.getTeam(req.authUser, req.params.id)) } /** @@ -30,10 +30,10 @@ async function getTeam(req, res) { * @param req the request * @param res the response */ -async function getTeamJob(req, res) { +async function getTeamJob (req, res) { res.send( await service.getTeamJob(req.authUser, req.params.id, req.params.jobId) - ); + ) } /** @@ -41,9 +41,9 @@ async function getTeamJob(req, res) { * @param req the request * @param res the response */ -async function sendEmail(req, res) { - await service.sendEmail(req.authUser, req.body); - res.status(HttpStatus.NO_CONTENT).end(); +async function sendEmail (req, res) { + await service.sendEmail(req.authUser, req.body) + res.status(HttpStatus.NO_CONTENT).end() } /** @@ -51,10 +51,10 @@ async function sendEmail(req, res) { * @param req the request * @param res the response */ -async function addMembers(req, res) { +async function addMembers (req, res) { res.send( await service.addMembers(req.authUser, req.params.id, req.query, req.body) - ); + ) } /** @@ -62,13 +62,13 @@ async function addMembers(req, res) { * @param req the request * @param res the response */ -async function searchMembers(req, res) { +async function searchMembers (req, res) { const result = await service.searchMembers( req.authUser, req.params.id, req.query - ); - res.send(result.result); + ) + res.send(result.result) } /** @@ -76,13 +76,13 @@ async function searchMembers(req, res) { * @param req the request * @param res the response */ -async function searchInvites(req, res) { +async function searchInvites (req, res) { const result = await service.searchInvites( req.authUser, req.params.id, req.query - ); - res.send(result.result); + ) + res.send(result.result) } /** @@ -90,13 +90,13 @@ async function searchInvites(req, res) { * @param req the request * @param res the response */ -async function deleteMember(req, res) { +async function deleteMember (req, res) { await service.deleteMember( req.authUser, req.params.id, req.params.projectMemberId - ); - res.status(HttpStatus.NO_CONTENT).end(); + ) + res.status(HttpStatus.NO_CONTENT).end() } /** @@ -104,8 +104,8 @@ async function deleteMember(req, res) { * @param req the request * @param res the response */ -async function getMe(req, res) { - res.send(await service.getMe(req.authUser)); +async function getMe (req, res) { + res.send(await service.getMe(req.authUser)) } /** @@ -113,8 +113,8 @@ async function getMe(req, res) { * @param req the request * @param res the response */ -async function createProj(req, res) { - res.send(await service.createProj(req.authUser, req.body)); +async function createProj (req, res) { + res.send(await service.createProj(req.authUser, req.body)) } module.exports = { @@ -127,5 +127,5 @@ module.exports = { searchInvites, deleteMember, getMe, - createProj, -}; + createProj +} diff --git a/src/eventHandlers/RoleEventHandler.js b/src/eventHandlers/RoleEventHandler.js new file mode 100644 index 00000000..38dbdb79 --- /dev/null +++ b/src/eventHandlers/RoleEventHandler.js @@ -0,0 +1,64 @@ +/* + * Handle events for ResourceBooking. + */ + +const { Op } = require('sequelize') +const _ = require('lodash') +const models = require('../models') +const logger = require('../common/logger') +const helper = require('../common/helper') +const JobService = require('../services/JobService') + +const Job = models.Job + +/** + * When a Role is deleted, jobs related to + * that role should be updated + * @param {object} payload the event payload + * @returns {undefined} + */ +async function updateJobs (payload) { + // find jobs have this role + const jobs = await Job.findAll({ + where: { + roleIds: { [Op.contains]: [payload.value.id] } + }, + raw: true + }) + if (jobs.length === 0) { + logger.debug({ + component: 'RoleEventHandler', + context: 'updateJobs', + message: `id: ${payload.value.id} role has no related job - ignored` + }) + return + } + const m2mUser = helper.getAuditM2Muser() + // remove role id from related jobs + await Promise.all(_.map(jobs, async job => { + let roleIds = _.filter(job.roleIds, roleId => roleId !== payload.value.id) + if (roleIds.length === 0) { + roleIds = null + } + await JobService.partiallyUpdateJob(m2mUser, job.id, { roleIds }) + })) + logger.debug({ + component: 'RoleEventHandler', + context: 'updateJobs', + message: `role id: ${payload.value.id} removed from jobs with id: ${_.map(jobs, 'id')}` + }) +} + +/** + * Process role delete event. + * + * @param {Object} payload the event payload + * @returns {undefined} + */ +async function processDelete (payload) { + await updateJobs(payload) +} + +module.exports = { + processDelete +} diff --git a/src/eventHandlers/index.js b/src/eventHandlers/index.js index 17445994..6e0ec2a8 100644 --- a/src/eventHandlers/index.js +++ b/src/eventHandlers/index.js @@ -8,6 +8,7 @@ const JobEventHandler = require('./JobEventHandler') const JobCandidateEventHandler = require('./JobCandidateEventHandler') const ResourceBookingEventHandler = require('./ResourceBookingEventHandler') const InterviewEventHandler = require('./InterviewEventHandler') +const RoleEventHandler = require('./RoleEventHandler') const logger = require('../common/logger') const TopicOperationMapping = { @@ -16,7 +17,8 @@ const TopicOperationMapping = { [config.TAAS_RESOURCE_BOOKING_CREATE_TOPIC]: ResourceBookingEventHandler.processCreate, [config.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC]: ResourceBookingEventHandler.processUpdate, [config.TAAS_RESOURCE_BOOKING_DELETE_TOPIC]: ResourceBookingEventHandler.processDelete, - [config.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewEventHandler.processRequest + [config.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewEventHandler.processRequest, + [config.TAAS_ROLE_DELETE_TOPIC]: RoleEventHandler.processDelete } /** diff --git a/src/models/Job.js b/src/models/Job.js index 49d34ff7..66f15b0d 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -104,6 +104,12 @@ module.exports = (sequelize) => { defaultValue: false, allowNull: false }, + roleIds: { + field: 'role_ids', + type: Sequelize.ARRAY({ + type: Sequelize.UUID + }) + }, createdBy: { field: 'created_by', type: Sequelize.UUID, diff --git a/src/models/Role.js b/src/models/Role.js new file mode 100644 index 00000000..57cd5025 --- /dev/null +++ b/src/models/Role.js @@ -0,0 +1,165 @@ +const { Sequelize, Model } = require('sequelize') +const config = require('config') +const errors = require('../common/errors') + +module.exports = (sequelize) => { + class Role extends Model { + /** + * Get role by id + * @param {String} id the role id + * @returns {Role} the role instance + */ + static async findById (id) { + const role = await Role.findOne({ + where: { + id + } + }) + if (!role) { + throw new errors.NotFoundError(`id: ${id} "Role" doesn't exists.`) + } + return role + } + } + Role.init( + { + id: { + type: Sequelize.UUID, + primaryKey: true, + allowNull: false, + defaultValue: Sequelize.UUIDV4 + }, + name: { + type: Sequelize.STRING(50), + allowNull: false + }, + description: { + type: Sequelize.STRING(1000) + }, + listOfSkills: { + field: 'list_of_skills', + type: Sequelize.ARRAY({ + type: Sequelize.STRING(50) + }) + }, + rates: { + type: Sequelize.ARRAY({ + type: Sequelize.JSONB({ + global: { + type: Sequelize.SMALLINT, + allowNull: false + }, + inCountry: { + field: 'in_country', + type: Sequelize.SMALLINT, + allowNull: false + }, + offShore: { + field: 'off_shore', + type: Sequelize.SMALLINT, + allowNull: false + }, + rate30Global: { + field: 'rate30_global', + type: Sequelize.SMALLINT + }, + rate30InCountry: { + field: 'rate30_in_country', + type: Sequelize.SMALLINT + }, + rate30OffShore: { + field: 'rate30_off_shore', + type: Sequelize.SMALLINT + }, + rate20Global: { + field: 'rate20_global', + type: Sequelize.SMALLINT + }, + rate20InCountry: { + field: 'rate20_in_country', + type: Sequelize.SMALLINT + }, + rate20OffShore: { + field: 'rate20_off_shore', + type: Sequelize.SMALLINT + } + }), + allowNull: false + }), + allowNull: false + }, + numberOfMembers: { + field: 'number_of_members', + type: Sequelize.NUMERIC + }, + numberOfMembersAvailable: { + field: 'number_of_members_available', + type: Sequelize.SMALLINT + }, + imageUrl: { + field: 'image_url', + type: Sequelize.STRING(255) + }, + timeToCandidate: { + field: 'time_to_candidate', + type: Sequelize.SMALLINT + }, + timeToInterview: { + field: 'time_to_interview', + type: Sequelize.SMALLINT + }, + createdBy: { + field: 'created_by', + type: Sequelize.UUID, + allowNull: false + }, + updatedBy: { + field: 'updated_by', + type: Sequelize.UUID + }, + createdAt: { + field: 'created_at', + type: Sequelize.DATE + }, + updatedAt: { + field: 'updated_at', + type: Sequelize.DATE + }, + deletedAt: { + field: 'deleted_at', + type: Sequelize.DATE + } + }, + { + schema: config.DB_SCHEMA_NAME, + sequelize, + tableName: 'roles', + paranoid: true, + deletedAt: 'deletedAt', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + timestamps: true, + defaultScope: { + attributes: { + exclude: ['deletedAt'] + } + }, + hooks: { + afterCreate: (role) => { + delete role.dataValues.deletedAt + } + }, + indexes: [ + { + unique: true, + fields: ['name'], + where: { + deleted_at: null + } + } + ] + } + ) + + return Role +} diff --git a/src/routes/RoleRoutes.js b/src/routes/RoleRoutes.js new file mode 100644 index 00000000..2fb6d55b --- /dev/null +++ b/src/routes/RoleRoutes.js @@ -0,0 +1,41 @@ +/** + * Contains role routes + */ +const constants = require('../../app-constants') + +module.exports = { + '/roles': { + post: { + controller: 'RoleController', + method: 'createRole', + auth: 'jwt', + scopes: [constants.Scopes.CREATE_ROLE, constants.Scopes.ALL_ROLE] + }, + get: { + controller: 'RoleController', + method: 'searchRoles', + auth: 'jwt', + scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] + } + }, + '/roles/:id': { + get: { + controller: 'RoleController', + method: 'getRole', + auth: 'jwt', + scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] + }, + patch: { + controller: 'RoleController', + method: 'updateRole', + auth: 'jwt', + scopes: [constants.Scopes.UPDATE_ROLE, constants.Scopes.ALL_ROLE] + }, + delete: { + controller: 'RoleController', + method: 'deleteRole', + auth: 'jwt', + scopes: [constants.Scopes.DELETE_ROLE, constants.Scopes.ALL_ROLE] + } + } +} diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index 9bbe25c6..07d777d4 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -1,7 +1,7 @@ /** * Contains taas team routes */ -const constants = require('../../app-constants'); +const constants = require('../../app-constants') module.exports = { '/taas-teams': { @@ -9,85 +9,85 @@ module.exports = { controller: 'TeamController', method: 'searchTeams', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/email': { post: { controller: 'TeamController', method: 'sendEmail', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/skills': { get: { controller: 'SkillController', method: 'searchSkills', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/me': { get: { controller: 'TeamController', method: 'getMe', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id': { get: { controller: 'TeamController', method: 'getTeam', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/jobs/:jobId': { get: { controller: 'TeamController', method: 'getTeamJob', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/members': { post: { controller: 'TeamController', method: 'addMembers', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], + scopes: [constants.Scopes.READ_TAAS_TEAM] }, get: { controller: 'TeamController', method: 'searchMembers', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/invites': { get: { controller: 'TeamController', method: 'searchInvites', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/members/:projectMemberId': { delete: { controller: 'TeamController', method: 'deleteMember', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/createTeamRequest': { post: { controller: 'TeamController', method: 'createProj', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, - }, -}; + scopes: [constants.Scopes.READ_TAAS_TEAM] + } + } +} diff --git a/src/services/InterviewService.js b/src/services/InterviewService.js index 10a065f4..a69a788c 100644 --- a/src/services/InterviewService.js +++ b/src/services/InterviewService.js @@ -241,8 +241,8 @@ async function requestInterview (currentUser, jobCandidateId, interview) { const guestMembers = await helper.getMemberDetailsByEmails(interview.guestEmails) interview.hostName = `${hostMembers[0].firstName} ${hostMembers[0].lastName}` interview.guestNames = _.map(interview.guestEmails, (guestEmail) => { - var foundGuestMember = _.find(guestMembers, function(guestMember) { return guestEmail == guestMember.email }); - return (foundGuestMember != undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split("@")[0] + var foundGuestMember = _.find(guestMembers, function (guestMember) { return guestEmail === guestMember.email }) + return (foundGuestMember !== undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split('@')[0] }) try { diff --git a/src/services/JobService.js b/src/services/JobService.js index 7d855bd0..be5dfdec 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -74,6 +74,27 @@ async function _validateSkills (skills) { } } +/** + * Validate if all roles exist. + * + * @param {Array} roles the list of roles + * @returns {undefined} + */ +async function _validateRoles (roles) { + const foundRolesObj = await models.Role.findAll({ + where: { + id: roles + }, + attributes: ['id'], + raw: true + }) + const foundRoles = _.map(foundRolesObj, 'id') + const nonexistentRoles = _.difference(roles, foundRoles) + if (nonexistentRoles.length > 0) { + throw new errors.BadRequestError(`Invalid roles: [${nonexistentRoles}]`) + } +} + /** * Check user permission for getting job. * @@ -154,6 +175,10 @@ async function createJob (currentUser, job) { } await _validateSkills(job.skills) + if (job.roleIds) { + job.roleIds = _.uniq(job.roleIds) + await _validateRoles(job.roleIds) + } job.id = uuid() job.createdBy = await helper.getUserId(currentUser.userId) @@ -177,7 +202,8 @@ createJob.schema = Joi.object().keys({ rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()).required(), - isApplicationPageActive: Joi.boolean() + isApplicationPageActive: Joi.boolean(), + roleIds: Joi.array().items(Joi.string().uuid().required()) }).required() }).required() @@ -192,6 +218,10 @@ async function updateJob (currentUser, id, data) { if (data.skills) { await _validateSkills(data.skills) } + if (data.roleIds) { + data.roleIds = _.uniq(data.roleIds) + await _validateRoles(data.roleIds) + } let job = await Job.findById(id) const oldValue = job.toJSON() @@ -245,7 +275,8 @@ partiallyUpdateJob.schema = Joi.object().keys({ rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()), - isApplicationPageActive: Joi.boolean() + isApplicationPageActive: Joi.boolean(), + roleIds: Joi.array().items(Joi.string().uuid().required()).allow(null) }).required() }).required() @@ -276,7 +307,8 @@ fullyUpdateJob.schema = Joi.object().keys({ workload: Joi.workload().allow(null).default(null), skills: Joi.array().items(Joi.string().uuid()).required(), status: Joi.jobStatus().default('sourcing'), - isApplicationPageActive: Joi.boolean() + isApplicationPageActive: Joi.boolean(), + roleIds: Joi.array().items(Joi.string().uuid().required()).default(null) }).required() }).required() @@ -444,9 +476,9 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } [Op.like]: `%${criteria.title}%` } } - if (criteria.skills) { + if (criteria.skill) { filter.skills = { - [Op.contains]: [criteria.skills] + [Op.contains]: [criteria.skill] } } const jobs = await Job.findAll({ diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index f5c40206..fd3d7773 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unreachable */ /** * This service provides operations of ResourceBooking. */ diff --git a/src/services/RoleService.js b/src/services/RoleService.js new file mode 100644 index 00000000..19006f67 --- /dev/null +++ b/src/services/RoleService.js @@ -0,0 +1,305 @@ +/** + * This service provides operations of Roles. + */ + +const _ = require('lodash') +const config = require('config') +const Joi = require('joi') +const { Op } = require('sequelize') +const uuid = require('uuid') +const helper = require('../common/helper') +const logger = require('../common/logger') +const errors = require('../common/errors') +const models = require('../models') + +const Role = models.Role +const esClient = helper.getESClient() + +/** + * Check user permission for deleting, creating or updating role. + * @param {Object} currentUser the user who perform this operation. + * @returns {undefined} + */ +async function _checkUserPermissionForWriteDeleteRole (currentUser) { + if (!currentUser.hasManagePermission && !currentUser.isMachine) { + throw new errors.ForbiddenError('You are not allowed to perform this action!') + } +} + +/** + * Cleans and validates skill names using skills service + * @param {Array} skills array of skill names to validate + * @returns {undefined} + */ +async function _cleanAndValidateSkillNames (skills) { + // remove duplicates, leading and trailing whitespaces, remove empties and convert to lowercase. + const cleanedSkills = _.uniq(_.filter(_.map(skills, skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill))) + if (cleanedSkills.length > 0) { + // search skills if they are exists + const { result } = await helper.getTopcoderSkills({ name: _.join(cleanedSkills, ',') }) + const skillNames = _.map(result, 'name') + // find skills that not valid + const unValidSkills = _.differenceWith(cleanedSkills, skillNames, (a, b) => _.toLower(a) === _.toLower(b)) + if (unValidSkills.length > 0) { + throw new errors.BadRequestError(`skills: "${unValidSkills}" are not valid`) + } + return cleanedSkills + } else { + return null + } +} + +/** + * Check user permission for deleting, creating or updating role. + * @param {Object} currentUser the user who perform this operation. + * @returns {undefined} + */ +async function _checkIfSameNamedRoleExists (roleName) { + // We can't create another Role with the same name + const role = await Role.findOne({ + where: { + name: { [Op.iLike]: roleName } + }, + raw: true + }) + if (role) { + throw new errors.BadRequestError(`Role: "${role.name}" is already exists.`) + } +} + +/** + * Get role by id + * @param {Object} currentUser the user who perform this operation. + * @param {String} id the role id + * @param {Boolean} fromDb flag if query db for data or not + * @returns {Object} the role + */ +async function getRole (currentUser, id, fromDb = false) { + if (!fromDb) { + try { + const role = await esClient.get({ + index: config.esConfig.ES_INDEX_ROLE, + id + }) + return { id: role.body._id, ...role.body._source } + } catch (err) { + if (helper.isDocumentMissingException(err)) { + throw new errors.NotFoundError(`id: ${id} "Role" not found`) + } + } + } + logger.info({ component: 'RoleService', context: 'getRole', message: 'try to query db for data' }) + const role = await Role.findById(id) + + return role.toJSON() +} + +getRole.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.string().uuid().required(), + fromDb: Joi.boolean() +}).required() + +/** + * Create role + * @param {Object} currentUser the user who perform this operation + * @param {Object} role the role to be created + * @returns {Object} the created role + */ +async function createRole (currentUser, role) { + // check permission + await _checkUserPermissionForWriteDeleteRole(currentUser) + // check if another Role with the same name exists. + await _checkIfSameNamedRoleExists(role.name) + // clean and validate skill names + if (role.listOfSkills) { + role.listOfSkills = await _cleanAndValidateSkillNames(role.listOfSkills) + } + + role.id = uuid.v4() + role.createdBy = await helper.getUserId(currentUser.userId) + + const created = await Role.create(role) + + await helper.postEvent(config.TAAS_ROLE_CREATE_TOPIC, created.toJSON()) + return created.toJSON() +} + +createRole.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + role: Joi.object().keys({ + name: Joi.string().max(50).required(), + description: Joi.string().max(1000), + listOfSkills: Joi.array().items(Joi.string().max(50).required()), + rates: Joi.array().items(Joi.object().keys({ + global: Joi.smallint().required(), + inCountry: Joi.smallint().required(), + offShore: Joi.smallint().required(), + rate30Global: Joi.smallint(), + rate30InCountry: Joi.smallint(), + rate30OffShore: Joi.smallint(), + rate20Global: Joi.smallint(), + rate20InCountry: Joi.smallint(), + rate20OffShore: Joi.smallint() + }).required()).required(), + numberOfMembers: Joi.number(), + numberOfMembersAvailable: Joi.smallint(), + imageUrl: Joi.string().uri().max(255), + timeToCandidate: Joi.smallint(), + timeToInterview: Joi.smallint() + }).required() +}).required() + +/** + * Partially Update role + * @param {Object} currentUser the user who perform this operation + * @param {String} id the role id + * @param {Object} data the data to be updated + * @returns {Object} the updated role + */ +async function updateRole (currentUser, id, data) { + // check permission + await _checkUserPermissionForWriteDeleteRole(currentUser) + + const role = await Role.findById(id) + const oldValue = role.toJSON() + // if name is changed, check if another Role with the same name exists. + if (data.name && data.name.toLowerCase() !== role.dataValues.name.toLowerCase()) { + await _checkIfSameNamedRoleExists(data.name) + } + // clean and validate skill names + if (data.listOfSkills) { + data.listOfSkills = await _cleanAndValidateSkillNames(data.listOfSkills) + } + + data.updatedBy = await helper.getUserId(currentUser.userId) + const updated = await role.update(data) + + await helper.postEvent(config.TAAS_ROLE_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) + return updated.toJSON() +} + +updateRole.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.string().uuid().required(), + data: Joi.object().keys({ + name: Joi.string().max(50), + description: Joi.string().max(1000).allow(null), + listOfSkills: Joi.array().items(Joi.string().max(50).required()).allow(null), + rates: Joi.array().items(Joi.object().keys({ + global: Joi.smallint().required(), + inCountry: Joi.smallint().required(), + offShore: Joi.smallint().required(), + rate30Global: Joi.smallint(), + rate30InCountry: Joi.smallint(), + rate30OffShore: Joi.smallint(), + rate20Global: Joi.smallint(), + rate20InCountry: Joi.smallint(), + rate20OffShore: Joi.smallint() + }).required()), + numberOfMembers: Joi.number().allow(null), + numberOfMembersAvailable: Joi.smallint().allow(null), + imageUrl: Joi.string().uri().max(255).allow(null), + timeToCandidate: Joi.smallint().allow(null), + timeToInterview: Joi.smallint().allow(null) + }).required() +}).required() + +/** + * Delete role by id + * @param {Object} currentUser the user who perform this operation + * @param {String} id the role id + */ +async function deleteRole (currentUser, id) { + // check permission + await _checkUserPermissionForWriteDeleteRole(currentUser) + + const role = await Role.findById(id) + await role.destroy() + await helper.postEvent(config.TAAS_ROLE_DELETE_TOPIC, { id }) +} + +deleteRole.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.string().uuid().required() +}).required() + +/** + * List roles + * @param {Object} currentUser the user who perform this operation. + * @param {Object} criteria the search criteria + * @returns {Object} the search result + */ +async function searchRoles (currentUser, criteria) { + // clean skill names and convert into an array + criteria.skillsList = _.filter(_.map(_.split(_.trim(criteria.skillsList), ','), skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill)) + try { + const esQuery = { + index: config.get('esConfig.ES_INDEX_ROLE'), + body: { + query: { + bool: { + must: [] + } + }, + size: 10000, + sort: [{ name: { order: 'asc' } }] + } + } + // Apply skill name filters. listOfSkills array should include all skills provided in criteria. + _.each(criteria.skillsList, skill => { + esQuery.body.query.bool.must.push({ + term: { + listOfSkills: skill + } + }) + }) + // Apply name filter, allow partial match + if (criteria.keyword) { + esQuery.body.query.bool.must.push({ + wildcard: { + name: `*${criteria.keyword}*` + + } + }) + } + logger.debug({ component: 'RoleService', context: 'searchRoles', message: `Query: ${JSON.stringify(esQuery)}` }) + + const { body } = await esClient.search(esQuery) + return _.map(body.hits.hits, (hit) => _.assign(hit._source, { id: hit._id })) + } catch (err) { + logger.logFullError(err, { component: 'RoleService', context: 'searchRoles' }) + } + logger.info({ component: 'RoleService', context: 'searchRoles', message: 'fallback to DB query' }) + const filter = { [Op.and]: [] } + // Apply skill name filters. listOfSkills array should include all skills provided in criteria. + if (criteria.skillsList) { + filter[Op.and].push({ listOfSkills: { [Op.contains]: criteria.skillsList } }) + } + // Apply name filter, allow partial match and ignore case + if (criteria.keyword) { + filter[Op.and].push({ name: { [Op.iLike]: `%${criteria.keyword}%` } }) + } + const queryCriteria = { + where: filter, + order: [['name', 'asc']] + } + const roles = await Role.findAll(queryCriteria) + return roles +} + +searchRoles.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + criteria: Joi.object().keys({ + skillsList: Joi.string(), + keyword: Joi.string() + }).required() +}).required() + +module.exports = { + getRole, + createRole, + updateRole, + deleteRole, + searchRoles +} diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 3f6dbfd3..4052e942 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -2,16 +2,16 @@ * This service provides operations of Job. */ -const _ = require('lodash'); -const Joi = require('joi'); -const dateFNS = require('date-fns'); -const config = require('config'); -const emailTemplateConfig = require('../../config/email_template.config'); -const helper = require('../common/helper'); -const logger = require('../common/logger'); -const errors = require('../common/errors'); -const JobService = require('./JobService'); -const ResourceBookingService = require('./ResourceBookingService'); +const _ = require('lodash') +const Joi = require('joi') +const dateFNS = require('date-fns') +const config = require('config') +const emailTemplateConfig = require('../../config/email_template.config') +const helper = require('../common/helper') +const logger = require('../common/logger') +const errors = require('../common/errors') +const JobService = require('./JobService') +const ResourceBookingService = require('./ResourceBookingService') const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { return { @@ -20,9 +20,9 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { from: template.from, recipients: template.recipients, cc: template.cc, - sendgridTemplateId: template.sendgridTemplateId, - }; -}); + sendgridTemplateId: template.sendgridTemplateId + } +}) /** * Function to get placed resource bookings with specific projectIds @@ -30,14 +30,14 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { * @param {Array} projectIds project ids * @returns the request result */ -async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { - const criteria = { status: 'placed', projectIds }; +async function _getPlacedResourceBookingsByProjectIds (currentUser, projectIds) { + const criteria = { status: 'placed', projectIds } const { result } = await ResourceBookingService.searchResourceBookings( currentUser, criteria, { returnAll: true } - ); - return result; + ) + return result } /** @@ -46,13 +46,13 @@ async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { * @param {Array} projectIds project ids * @returns the request result */ -async function _getJobsByProjectIds(currentUser, projectIds) { +async function _getJobsByProjectIds (currentUser, projectIds) { const { result } = await JobService.searchJobs( currentUser, { projectIds }, { returnAll: true } - ); - return result; + ) + return result } /** @@ -61,26 +61,26 @@ async function _getJobsByProjectIds(currentUser, projectIds) { * @param {Object} criteria the search criteria * @returns {Object} the search result, contain total/page/perPage and result array */ -async function searchTeams(currentUser, criteria) { - const sort = `${criteria.sortBy} ${criteria.sortOrder}`; +async function searchTeams (currentUser, criteria) { + const sort = `${criteria.sortBy} ${criteria.sortOrder}` // Get projects from /v5/projects with searching criteria const { total, page, perPage, - result: projects, + result: projects } = await helper.getProjects(currentUser, { page: criteria.page, perPage: criteria.perPage, name: criteria.name, - sort, - }); + sort + }) return { total, page, perPage, - result: await getTeamDetail(currentUser, projects), - }; + result: await getTeamDetail(currentUser, projects) + } } searchTeams.schema = Joi.object() @@ -107,13 +107,13 @@ searchTeams.schema = Joi.object() then: Joi.forbidden().label( 'sortOrder(with sortBy being `best match`)' ), - otherwise: Joi.string().valid('asc', 'desc').default('desc'), + otherwise: Joi.string().valid('asc', 'desc').default('desc') }), - name: Joi.string(), + name: Joi.string() }) - .required(), + .required() }) - .required(); + .required() /** * Get team details @@ -122,69 +122,69 @@ searchTeams.schema = Joi.object() * @param {Object} isSearch the flag whether for search function * @returns {Object} the search result */ -async function getTeamDetail(currentUser, projects, isSearch = true) { - const projectIds = _.map(projects, 'id'); +async function getTeamDetail (currentUser, projects, isSearch = true) { + const projectIds = _.map(projects, 'id') // Get all placed resourceBookings filtered by projectIds const resourceBookings = await _getPlacedResourceBookingsByProjectIds( currentUser, projectIds - ); + ) // Get all jobs filtered by projectIds - const jobs = await _getJobsByProjectIds(currentUser, projectIds); + const jobs = await _getJobsByProjectIds(currentUser, projectIds) // Get first week day and last week day - const curr = new Date(); - const firstDay = dateFNS.startOfWeek(curr); - const lastDay = dateFNS.endOfWeek(curr); + const curr = new Date() + const firstDay = dateFNS.startOfWeek(curr) + const lastDay = dateFNS.endOfWeek(curr) logger.debug({ component: 'TeamService', context: 'getTeamDetail', - message: `week started: ${firstDay}, week ended: ${lastDay}`, - }); + message: `week started: ${firstDay}, week ended: ${lastDay}` + }) - const result = []; + const result = [] for (const project of projects) { - const rbs = _.filter(resourceBookings, { projectId: project.id }); - const res = _.clone(project); - res.weeklyCost = 0; - res.resources = []; + const rbs = _.filter(resourceBookings, { projectId: project.id }) + const res = _.clone(project) + res.weeklyCost = 0 + res.resources = [] if (rbs && rbs.length > 0) { // Get minimal start date and maximal end date - const startDates = []; - const endDates = []; + const startDates = [] + const endDates = [] for (const rbsItem of rbs) { if (rbsItem.startDate) { - startDates.push(new Date(rbsItem.startDate)); + startDates.push(new Date(rbsItem.startDate)) } if (rbsItem.endDate) { - endDates.push(new Date(rbsItem.endDate)); + endDates.push(new Date(rbsItem.endDate)) } } if (startDates && startDates.length > 0) { - res.startDate = _.min(startDates); + res.startDate = _.min(startDates) } if (endDates && endDates.length > 0) { - res.endDate = _.max(endDates); + res.endDate = _.max(endDates) } // Count weekly rate for (const item of rbs) { // ignore any resourceBooking that has customerRate missed if (!item.customerRate) { - continue; + continue } - const startDate = new Date(item.startDate); - const endDate = new Date(item.endDate); + const startDate = new Date(item.startDate) + const endDate = new Date(item.endDate) // normally startDate is smaller than endDate for a resourceBooking so not check if startDate < endDate if ( (!item.startDate || startDate < lastDay) && (!item.endDate || endDate > firstDay) ) { - res.weeklyCost += item.customerRate; + res.weeklyCost += item.customerRate } } @@ -194,48 +194,48 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { const resource = { id: rb.id, userId: user.id, - ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']), - }; + ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']) + } // If call function is not search, add jobId field if (!isSearch) { - resource.jobId = rb.jobId; - resource.customerRate = rb.customerRate; - resource.startDate = rb.startDate; - resource.endDate = rb.endDate; + resource.jobId = rb.jobId + resource.customerRate = rb.customerRate + resource.startDate = rb.startDate + resource.endDate = rb.endDate } - return resource; - }); + return resource + }) }) - ); + ) if (resourceInfos && resourceInfos.length > 0) { - res.resources = resourceInfos; + res.resources = resourceInfos - const userHandles = _.map(resourceInfos, 'handle'); + const userHandles = _.map(resourceInfos, 'handle') // Get user photo from /v5/members - const members = await helper.getMembers(userHandles); + const members = await helper.getMembers(userHandles) for (const item of res.resources) { const findMember = _.find(members, { - handleLower: item.handle.toLowerCase(), - }); + handleLower: item.handle.toLowerCase() + }) if (findMember && findMember.photoURL) { - item.photo_url = findMember.photoURL; + item.photo_url = findMember.photoURL } } } } - const jobsTmp = _.filter(jobs, { projectId: project.id }); + const jobsTmp = _.filter(jobs, { projectId: project.id }) if (jobsTmp && jobsTmp.length > 0) { if (isSearch) { // Count total positions - res.totalPositions = 0; + res.totalPositions = 0 for (const item of jobsTmp) { // only sum numPositions of jobs whose status is NOT cancelled or closed if (['cancelled', 'closed'].includes(item.status)) { - continue; + continue } - res.totalPositions += item.numPositions; + res.totalPositions += item.numPositions } } else { res.jobs = _.map(jobsTmp, (job) => { @@ -249,15 +249,15 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { 'skills', 'customerRate', 'status', - 'title', - ]); - }); + 'title' + ]) + }) } } - result.push(res); + result.push(res) } - return result; + return result } /** @@ -266,35 +266,35 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { * @param {String} id the job id * @returns {Object} the team */ -async function getTeam(currentUser, id) { - const project = await helper.getProjectById(currentUser, id); - const result = await getTeamDetail(currentUser, [project], false); - const teamDetail = result[0]; +async function getTeam (currentUser, id) { + const project = await helper.getProjectById(currentUser, id) + const result = await getTeamDetail(currentUser, [project], false) + const teamDetail = result[0] // add job skills for result - let jobSkills = []; + let jobSkills = [] if (teamDetail && teamDetail.jobs) { for (const job of teamDetail.jobs) { if (job.skills) { - const usersPromises = []; + const usersPromises = [] _.map(job.skills, (skillId) => { - usersPromises.push(helper.getSkillById(skillId)); - }); - jobSkills = await Promise.all(usersPromises); - job.skills = jobSkills; + usersPromises.push(helper.getSkillById(skillId)) + }) + jobSkills = await Promise.all(usersPromises) + job.skills = jobSkills } } } - return teamDetail; + return teamDetail } getTeam.schema = Joi.object() .keys({ currentUser: Joi.object().required(), - id: Joi.number().integer().required(), + id: Joi.number().integer().required() }) - .required(); + .required() /** * Get team job with id @@ -303,25 +303,25 @@ getTeam.schema = Joi.object() * @param {String} jobId the job id * @returns the team job */ -async function getTeamJob(currentUser, id, jobId) { - const project = await helper.getProjectById(currentUser, id); - const jobs = await _getJobsByProjectIds(currentUser, [project.id]); - const job = _.find(jobs, { id: jobId }); +async function getTeamJob (currentUser, id, jobId) { + const project = await helper.getProjectById(currentUser, id) + const jobs = await _getJobsByProjectIds(currentUser, [project.id]) + const job = _.find(jobs, { id: jobId }) if (!job) { throw new errors.NotFoundError( `id: ${jobId} "Job" with Team id ${id} doesn't exist` - ); + ) } const result = { id: job.id, - title: job.title, - }; + title: job.title + } if (job.skills) { result.skills = await Promise.all( _.map(job.skills, (skillId) => helper.getSkillById(skillId)) - ); + ) } // If the job has candidates, the following data for each candidate would be populated: @@ -336,12 +336,12 @@ async function getTeamJob(currentUser, id, jobId) { _.map(_.uniq(_.map(job.candidates, 'userId')), (userId) => helper.getUserById(userId, true) ) - ); - const userMap = _.groupBy(users, 'id'); + ) + const userMap = _.groupBy(users, 'id') // find photo URLs for users - const members = await helper.getMembers(_.map(users, 'handle')); - const photoURLMap = _.groupBy(members, 'handleLower'); + const members = await helper.getMembers(_.map(users, 'handle')) + const photoURLMap = _.groupBy(members, 'handleLower') result.candidates = _.map(job.candidates, (candidate) => { const candidateData = _.pick(candidate, [ @@ -349,33 +349,33 @@ async function getTeamJob(currentUser, id, jobId) { 'resume', 'userId', 'interviews', - 'id', - ]); - const userData = userMap[candidate.userId][0]; + 'id' + ]) + const userData = userMap[candidate.userId][0] // attach user data to the candidate Object.assign( candidateData, _.pick(userData, ['handle', 'firstName', 'lastName', 'skills']) - ); + ) // attach photo URL to the candidate - const handleLower = userData.handle.toLowerCase(); + const handleLower = userData.handle.toLowerCase() if (photoURLMap[handleLower]) { - candidateData.photo_url = photoURLMap[handleLower][0].photoURL; + candidateData.photo_url = photoURLMap[handleLower][0].photoURL } - return candidateData; - }); + return candidateData + }) } - return result; + return result } getTeamJob.schema = Joi.object() .keys({ currentUser: Joi.object().required(), id: Joi.number().integer().required(), - jobId: Joi.string().guid().required(), + jobId: Joi.string().guid().required() }) - .required(); + .required() /** * Send email through a particular template @@ -383,21 +383,21 @@ getTeamJob.schema = Joi.object() * @param {Object} data the email object * @returns {undefined} */ -async function sendEmail(currentUser, data) { - const template = emailTemplates[data.template]; - const dataCC = data.cc || []; - const templateCC = template.cc || []; - const dataRecipients = data.recipients || []; - const templateRecipients = template.recipients || []; +async function sendEmail (currentUser, data) { + const template = emailTemplates[data.template] + const dataCC = data.cc || [] + const templateCC = template.cc || [] + const dataRecipients = data.recipients || [] + const templateRecipients = template.recipients || [] const subjectBody = { subject: data.subject || template.subject, - body: data.body || template.body, - }; + body: data.body || template.body + } for (const key in subjectBody) { subjectBody[key] = await helper.substituteStringByObject( subjectBody[key], data.data - ); + ) } const emailData = { // override template if coming data already have the 'from' address @@ -407,9 +407,9 @@ async function sendEmail(currentUser, data) { cc: _.uniq([...dataCC, ...templateCC]), data: { ...data.data, ...subjectBody }, sendgrid_template_id: template.sendgridTemplateId, - version: 'v3', - }; - await helper.postEvent(config.EMAIL_TOPIC, emailData); + version: 'v3' + } + await helper.postEvent(config.EMAIL_TOPIC, emailData) } sendEmail.schema = Joi.object() @@ -423,11 +423,11 @@ sendEmail.schema = Joi.object() data: Joi.object().required(), from: Joi.string().email(), recipients: Joi.array().items(Joi.string().email()).allow(null), - cc: Joi.array().items(Joi.string().email()).allow(null), + cc: Joi.array().items(Joi.string().email()).allow(null) }) - .required(), + .required() }) - .required(); + .required() /** * Add a member to a team as customer. @@ -437,25 +437,25 @@ sendEmail.schema = Joi.object() * @param {String} fields the fields to be returned * @returns {Object} the member added */ -async function _addMemberToProjectAsCustomer(projectId, userId, fields) { +async function _addMemberToProjectAsCustomer (projectId, userId, fields) { try { const member = await helper.createProjectMember( projectId, { userId: userId, role: 'customer' }, { fields } - ); - return member; + ) + return member } catch (err) { - err.message = _.get(err, 'response.body.message') || err.message; + err.message = _.get(err, 'response.body.message') || err.message if (err.message && err.message.includes('User already registered')) { - throw new Error('User is already added'); + throw new Error('User is already added') } logger.error({ component: 'TeamService', context: '_addMemberToProjectAsCustomer', - message: err.message, - }); - throw err; + message: err.message + }) + throw err } } @@ -467,16 +467,16 @@ async function _addMemberToProjectAsCustomer(projectId, userId, fields) { * @param {Object} data the object including members with handle/email to be added * @returns {Object} the success/failed added members */ -async function addMembers(currentUser, id, criteria, data) { - await helper.getProjectById(currentUser, id); // check whether the user can access the project +async function addMembers (currentUser, id, criteria, data) { + await helper.getProjectById(currentUser, id) // check whether the user can access the project const result = { success: [], - failed: [], - }; + failed: [] + } - const handles = data.handles || []; - const emails = data.emails || []; + const handles = data.handles || [] + const emails = data.emails || [] const handleMembers = await helper .getMemberDetailsByHandles(handles) @@ -484,9 +484,9 @@ async function addMembers(currentUser, id, criteria, data) { _.map(members, (member) => ({ ...member, // populate members with lower-cased handle for case insensitive search - handleLowerCase: member.handle.toLowerCase(), + handleLowerCase: member.handle.toLowerCase() })) - ); + ) const emailMembers = await helper .getMemberDetailsByEmails(emails) @@ -494,20 +494,20 @@ async function addMembers(currentUser, id, criteria, data) { _.map(members, (member) => ({ ...member, // populate members with lower-cased email for case insensitive search - emailLowerCase: member.email.toLowerCase(), + emailLowerCase: member.email.toLowerCase() })) - ); + ) await Promise.all([ Promise.all( handles.map((handle) => { const memberDetails = _.find(handleMembers, { - handleLowerCase: handle.toLowerCase(), - }); + handleLowerCase: handle.toLowerCase() + }) if (!memberDetails) { - result.failed.push({ error: "User doesn't exist", handle }); - return; + result.failed.push({ error: "User doesn't exist", handle }) + return } return _addMemberToProjectAsCustomer( @@ -517,23 +517,23 @@ async function addMembers(currentUser, id, criteria, data) { ) .then((member) => { // note, that we return `handle` in the same case it was in request - result.success.push({ ...member, handle }); + result.success.push({ ...member, handle }) }) .catch((err) => { - result.failed.push({ error: err.message, handle }); - }); + result.failed.push({ error: err.message, handle }) + }) }) ), Promise.all( emails.map((email) => { const memberDetails = _.find(emailMembers, { - emailLowerCase: email.toLowerCase(), - }); + emailLowerCase: email.toLowerCase() + }) if (!memberDetails) { - result.failed.push({ error: "User doesn't exist", email }); - return; + result.failed.push({ error: "User doesn't exist", email }) + return } return _addMemberToProjectAsCustomer( @@ -543,16 +543,16 @@ async function addMembers(currentUser, id, criteria, data) { ) .then((member) => { // note, that we return `email` in the same case it was in request - result.success.push({ ...member, email }); + result.success.push({ ...member, email }) }) .catch((err) => { - result.failed.push({ error: err.message, email }); - }); + result.failed.push({ error: err.message, email }) + }) }) - ), - ]); + ) + ]) - return result; + return result } addMembers.schema = Joi.object() @@ -561,18 +561,18 @@ addMembers.schema = Joi.object() id: Joi.number().integer().required(), criteria: Joi.object() .keys({ - fields: Joi.string(), + fields: Joi.string() }) .required(), data: Joi.object() .keys({ handles: Joi.array().items(Joi.string()), - emails: Joi.array().items(Joi.string().email()), + emails: Joi.array().items(Joi.string().email()) }) .or('handles', 'emails') - .required(), + .required() }) - .required(); + .required() /** * Search members in a team. @@ -583,9 +583,9 @@ addMembers.schema = Joi.object() * @params {Object} criteria the search criteria * @returns {Object} the search result */ -async function searchMembers(currentUser, id, criteria) { - const result = await helper.listProjectMembers(currentUser, id, criteria); - return { result }; +async function searchMembers (currentUser, id, criteria) { + const result = await helper.listProjectMembers(currentUser, id, criteria) + return { result } } searchMembers.schema = Joi.object() @@ -595,11 +595,11 @@ searchMembers.schema = Joi.object() criteria: Joi.object() .keys({ role: Joi.string(), - fields: Joi.string(), + fields: Joi.string() }) - .required(), + .required() }) - .required(); + .required() /** * Search member invites for a team. @@ -610,13 +610,13 @@ searchMembers.schema = Joi.object() * @params {Object} criteria the search criteria * @returns {Object} the search result */ -async function searchInvites(currentUser, id, criteria) { +async function searchInvites (currentUser, id, criteria) { const result = await helper.listProjectMemberInvites( currentUser, id, criteria - ); - return { result }; + ) + return { result } } searchInvites.schema = Joi.object() @@ -625,11 +625,11 @@ searchInvites.schema = Joi.object() id: Joi.number().integer().required(), criteria: Joi.object() .keys({ - fields: Joi.string(), + fields: Joi.string() }) - .required(), + .required() }) - .required(); + .required() /** * Remove a member from a team. @@ -640,17 +640,17 @@ searchInvites.schema = Joi.object() * @param {String} projectMemberId the id of the project member * @returns {undefined} */ -async function deleteMember(currentUser, id, projectMemberId) { - await helper.deleteProjectMember(currentUser, id, projectMemberId); +async function deleteMember (currentUser, id, projectMemberId) { + await helper.deleteProjectMember(currentUser, id, projectMemberId) } deleteMember.schema = Joi.object() .keys({ currentUser: Joi.object().required(), id: Joi.number().integer().required(), - projectMemberId: Joi.number().integer().required(), + projectMemberId: Joi.number().integer().required() }) - .required(); + .required() /** * Return details about the current user. @@ -659,31 +659,31 @@ deleteMember.schema = Joi.object() * @params {Object} criteria the search criteria * @returns {Object} the user data for current user */ -async function getMe(currentUser) { - return helper.getUserByExternalId(currentUser.userId); +async function getMe (currentUser) { + return helper.getUserByExternalId(currentUser.userId) } getMe.schema = Joi.object() .keys({ - currentUser: Joi.object().required(), + currentUser: Joi.object().required() }) - .required(); + .required() /** * @param {Object} currentUser the user performing the operation. * @param {Object} data project data * @returns {Object} the created project */ -async function createProj(currentUser, data) { - return helper.createProject(currentUser, data); +async function createProj (currentUser, data) { + return helper.createProject(currentUser, data) } createProj.schema = Joi.object() .keys({ currentUser: Joi.object().required(), - data: Joi.object().required(), + data: Joi.object().required() }) - .required(); + .required() module.exports = { searchTeams, @@ -695,5 +695,5 @@ module.exports = { searchInvites, deleteMember, getMe, - createProj, -}; + createProj +} diff --git a/taas-apis.patch b/taas-apis.patch new file mode 100644 index 00000000..dcec96d8 --- /dev/null +++ b/taas-apis.patch @@ -0,0 +1,9418 @@ +From 79e73a98f25a56e793aaadbb9255bf3a99ecedd5 Mon Sep 17 00:00:00 2001 +From: eisbilir +Date: Sat, 29 May 2021 00:14:42 +0300 +Subject: [PATCH] role endpoint added + +--- + README.md | 6 +- + app-constants.js | 8 +- + config/default.js | 9 + + data/demo-data.json | 260 +- + ...coder-bookings-api.postman_collection.json | 3799 ++++++++++++++++- + docs/swagger.yaml | 476 +++ + ...topcoder-bookings.postman_environment.json | 56 +- + local/kafka-client/topics.txt | 3 + + migrations/2021-05-27-1-role-table-create.js | 146 + + .../2021-05-27-2-job-add-roleIds-field.js | 19 + + package.json | 1 + + scripts/data/exportData.js | 2 +- + scripts/data/importData.js | 2 +- + scripts/es/createIndex.js | 3 +- + scripts/es/deleteIndex.js | 3 +- + scripts/es/reIndexAll.js | 1 + + scripts/es/reIndexRoles.js | 37 + + src/bootstrap.js | 3 +- + src/common/helper.js | 1115 ++--- + src/controllers/RoleController.js | 59 + + src/controllers/TeamController.js | 62 +- + src/eventHandlers/RoleEventHandler.js | 64 + + src/eventHandlers/index.js | 4 +- + src/models/Job.js | 6 + + src/models/Role.js | 165 + + src/routes/RoleRoutes.js | 41 + + src/routes/TeamRoutes.js | 48 +- + src/services/InterviewService.js | 4 +- + src/services/JobService.js | 42 +- + src/services/ResourceBookingService.js | 1 + + src/services/RoleService.js | 305 ++ + src/services/TeamService.js | 390 +- + 32 files changed, 6271 insertions(+), 869 deletions(-) + create mode 100644 migrations/2021-05-27-1-role-table-create.js + create mode 100644 migrations/2021-05-27-2-job-add-roleIds-field.js + create mode 100644 scripts/es/reIndexRoles.js + create mode 100644 src/controllers/RoleController.js + create mode 100644 src/eventHandlers/RoleEventHandler.js + create mode 100644 src/models/Role.js + create mode 100644 src/routes/RoleRoutes.js + create mode 100644 src/services/RoleService.js + +diff --git a/README.md b/README.md +index 5e3895c..aa36c62 100644 +--- a/README.md ++++ b/README.md +@@ -87,6 +87,9 @@ + tc-taas-es-processor | [2021-04-09T21:20:19.035Z] app INFO : Starting kafka consumer + tc-taas-es-processor | 2021-04-09T21:20:21.292Z INFO no-kafka-client Joined group taas-es-processor generationId 1 as no-kafka-client-076538fc-60dd-4ca4-a2b9-520bdf73bc9e + tc-taas-es-processor | 2021-04-09T21:20:21.293Z INFO no-kafka-client Elected as group leader ++ tc-taas-es-processor | 2021-04-09T21:20:21.449Z DEBUG no-kafka-client Subscribed to taas.role.update:0 offset 0 leader kafka:9093 ++ tc-taas-es-processor | 2021-04-09T21:20:21.450Z DEBUG no-kafka-client Subscribed to taas.role.delete:0 offset 0 leader kafka:9093 ++ tc-taas-es-processor | 2021-04-09T21:20:21.451Z DEBUG no-kafka-client Subscribed to taas.role.requested:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-04-09T21:20:21.452Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.create:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-04-09T21:20:21.455Z DEBUG no-kafka-client Subscribed to taas.job.create:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-04-09T21:20:21.456Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.delete:0 offset 0 leader kafka:9093 +@@ -103,7 +106,7 @@ + tc-taas-es-processor | 2021-04-09T21:20:21.473Z DEBUG no-kafka-client Subscribed to taas.job.update:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-04-09T21:20:21.474Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.update:0 offset 0 leader kafka:9093 + tc-taas-es-processor | [2021-04-09T21:20:21.475Z] app INFO : Initialized....... +- tc-taas-es-processor | [2021-04-09T21:20:21.479Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete,taas.workperiod.create,taas.workperiod.update,taas.workperiod.delete,taas.workperiodpayment.create,taas.workperiodpayment.update,taas.workperiodpayment.delete ++ tc-taas-es-processor | [2021-04-09T21:20:21.479Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete,taas.workperiod.create,taas.workperiod.update,taas.workperiod.delete,taas.workperiodpayment.create,taas.workperiodpayment.update,taas.interview.requested,taas.interview.update,taas.interview.bulkUpdate,taas.role.requested,taas.role.update,taas.role.delete + tc-taas-es-processor | [2021-04-09T21:20:21.480Z] app INFO : Kick Start....... + tc-taas-es-processor | ********** Topcoder Health Check DropIn listening on port 3001 + tc-taas-es-processor | Topcoder Health Check DropIn started and ready to roll +@@ -194,6 +197,7 @@ To be able to change and test `taas-es-processor` locally you can follow the nex + | `npm run index:jobs ` | Indexes job data from db into ES, if jobId is not given all data is indexed. Use `-- --force` flag to skip confirmation | + | `npm run index:job-candidates ` | Indexes job candidate data from db into ES, if jobCandidateId is not given all data is indexed. Use `-- --force` flag to skip confirmation | + | `npm run index:resource-bookings ` | Indexes resource bookings data from db into ES, if resourceBookingsId is not given all data is indexed. Use `-- --force` flag to skip confirmation | ++| `npm run index:roles ` | Indexes roles data from db into ES, if roleId is not given all data is indexed. Use `-- --force` flag to skip confirmation | + | `npm run services:up` | Start services via docker-compose for local development. | + | `npm run services:down` | Stop services via docker-compose for local development. | + | `npm run services:logs -- -f ` | View logs of some service inside docker-compose. | +diff --git a/app-constants.js b/app-constants.js +index 534e46d..9b57772 100644 +--- a/app-constants.js ++++ b/app-constants.js +@@ -49,7 +49,13 @@ const Scopes = { + READ_INTERVIEW: 'read:taas-interviews', + CREATE_INTERVIEW: 'create:taas-interviews', + UPDATE_INTERVIEW: 'update:taas-interviews', +- ALL_INTERVIEW: 'all:taas-interviews' ++ ALL_INTERVIEW: 'all:taas-interviews', ++ // role ++ READ_ROLE: 'read:taas-roles', ++ CREATE_ROLE: 'create:taas-roles', ++ UPDATE_ROLE: 'update:taas-roles', ++ DELETE_ROLE: 'delete:taas-roles', ++ ALL_ROLE: 'all:taas-roles' + } + + // Interview related constants +diff --git a/config/default.js b/config/default.js +index 2b5ca7b..cf2a8a4 100644 +--- a/config/default.js ++++ b/config/default.js +@@ -76,6 +76,8 @@ module.exports = { + ES_INDEX_JOB_CANDIDATE: process.env.ES_INDEX_JOB_CANDIDATE || 'job_candidate', + // the resource booking index + ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking', ++ // the role index ++ ES_INDEX_ROLE: process.env.ES_INDEX_ROLE || 'role', + + // the max bulk size in MB for ES indexing + MAX_BULK_REQUEST_SIZE_MB: process.env.MAX_BULK_REQUEST_SIZE_MB || 20, +@@ -131,6 +133,13 @@ module.exports = { + TAAS_INTERVIEW_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_UPDATE_TOPIC || 'taas.interview.update', + // the interview bulk update Kafka message topic + TAAS_INTERVIEW_BULK_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_BULK_UPDATE_TOPIC || 'taas.interview.bulkUpdate', ++ // topics for role service ++ // the create role entity Kafka message topic ++ TAAS_ROLE_CREATE_TOPIC: process.env.TAAS_ROLE_CREATE_TOPIC || 'taas.role.requested', ++ // the update role entity Kafka message topic ++ TAAS_ROLE_UPDATE_TOPIC: process.env.TAAS_ROLE_UPDATE_TOPIC || 'taas.role.update', ++ // the delete role entity Kafka message topic ++ TAAS_ROLE_DELETE_TOPIC: process.env.TAAS_ROLE_DELETE_TOPIC || 'taas.role.delete', + + // the Kafka message topic for sending email + EMAIL_TOPIC: process.env.EMAIL_TOPIC || 'external.action.email', +diff --git a/data/demo-data.json b/data/demo-data.json +index e073344..5f6c4c0 100644 +--- a/data/demo-data.json ++++ b/data/demo-data.json +@@ -20,6 +20,7 @@ + ], + "status": "in-review", + "isApplicationPageActive": false, ++ "roleIds": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:21:10.394Z", +@@ -45,6 +46,7 @@ + ], + "status": "in-review", + "isApplicationPageActive": false, ++ "roleIds": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:11:26.934Z", +@@ -70,6 +72,7 @@ + ], + "status": "in-review", + "isApplicationPageActive": false, ++ "roleIds": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:18.595Z", +@@ -95,6 +98,7 @@ + ], + "status": "in-review", + "isApplicationPageActive": false, ++ "roleIds": null, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:12:09.293Z", +@@ -181,18 +185,29 @@ + "interviews": [ + { + "id": "077aa2ca-5b60-4ad9-a965-1b37e08a5046", ++ "xaiId": null, + "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", +- "googleCalendarId": null, +- "customMessage": null, +- "xaiTemplate": "interview-30", ++ "calendarEventId": null, ++ "templateUrl": "interview-30", ++ "templateId": null, ++ "templateType": null, ++ "title": null, ++ "locationDetails": null, ++ "duration": null, + "round": 1, + "startTimestamp": null, +- "attendeesList": null, ++ "endTimestamp": null, ++ "hostName": null, ++ "hostEmail": null, ++ "guestNames": null, ++ "guestEmails": null, + "status": "Completed", ++ "rescheduleUrl": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:16:10.887Z", +- "updatedAt": "2021-05-09T21:16:10.887Z" ++ "updatedAt": "2021-05-09T21:16:10.887Z", ++ "deletedAt": null + } + ] + }, +@@ -210,33 +225,55 @@ + "interviews": [ + { + "id": "b1f7ba76-640f-47e2-9463-59e51b51ec60", ++ "xaiId": null, + "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", +- "googleCalendarId": "dummyId", +- "customMessage": "This is a custom message", +- "xaiTemplate": "interview-30", ++ "calendarEventId": null, ++ "templateUrl": "interview-30", ++ "templateId": null, ++ "templateType": null, ++ "title": null, ++ "locationDetails": null, ++ "duration": null, + "round": 2, + "startTimestamp": null, +- "attendeesList": null, ++ "endTimestamp": null, ++ "hostName": null, ++ "hostEmail": null, ++ "guestNames": null, ++ "guestEmails": null, + "status": "Scheduling", ++ "rescheduleUrl": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:17:23.517Z", +- "updatedAt": "2021-05-09T21:17:23.517Z" ++ "updatedAt": "2021-05-09T21:17:23.517Z", ++ "deletedAt": null + }, + { + "id": "3144fa65-ea1a-4bec-81b0-7cb1c8845826", ++ "xaiId": null, + "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", +- "googleCalendarId": null, +- "customMessage": null, +- "xaiTemplate": "interview-30", ++ "calendarEventId": null, ++ "templateUrl": "interview-30", ++ "templateId": null, ++ "templateType": null, ++ "title": null, ++ "locationDetails": null, ++ "duration": null, + "round": 1, + "startTimestamp": null, +- "attendeesList": null, ++ "endTimestamp": null, ++ "hostName": null, ++ "hostEmail": null, ++ "guestNames": null, ++ "guestEmails": null, + "status": "Completed", ++ "rescheduleUrl": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:16:39.019Z", +- "updatedAt": "2021-05-09T21:16:39.019Z" ++ "updatedAt": "2021-05-09T21:16:39.019Z", ++ "deletedAt": null + } + ] + }, +@@ -254,54 +291,81 @@ + "interviews": [ + { + "id": "976d23a9-5710-453f-99d9-f57a588bb610", ++ "xaiId": null, + "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", +- "googleCalendarId": "dummyId", +- "customMessage": "This is a custom message", +- "xaiTemplate": "interview-30", ++ "calendarEventId": null, ++ "templateUrl": "interview-30", ++ "templateId": null, ++ "templateType": null, ++ "title": null, ++ "locationDetails": null, ++ "duration": null, + "round": 3, + "startTimestamp": null, +- "attendeesList": [ +- "attendee1@yopmail.com", +- "attendee2@yopmail.com" +- ], ++ "endTimestamp": null, ++ "hostName": null, ++ "hostEmail": null, ++ "guestNames": null, ++ "guestEmails": null, + "status": "Scheduling", ++ "rescheduleUrl": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:21:28.713Z", +- "updatedAt": "2021-05-09T21:21:28.713Z" ++ "updatedAt": "2021-05-09T21:21:28.713Z", ++ "deletedAt": null + }, + { + "id": "a23e1bf2-1084-4cfe-a0d8-d83bc6fec655", ++ "xaiId": null, + "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", +- "googleCalendarId": "dummyId", +- "customMessage": "This is a custom message", +- "xaiTemplate": "interview-30", ++ "calendarEventId": null, ++ "templateUrl": "interview-30", ++ "templateId": null, ++ "templateType": null, ++ "title": null, ++ "locationDetails": null, ++ "duration": null, + "round": 2, + "startTimestamp": null, +- "attendeesList": [ +- "attendee1@yopmail.com", +- "attendee2@yopmail.com" +- ], ++ "endTimestamp": null, ++ "hostName": null, ++ "hostEmail": null, ++ "guestNames": null, ++ "guestEmails": null, + "status": "Scheduling", ++ "rescheduleUrl": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:21:22.428Z", +- "updatedAt": "2021-05-09T21:21:22.428Z" ++ "updatedAt": "2021-05-09T21:21:22.428Z", ++ "deletedAt": null + }, + { + "id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb", ++ "xaiId": null, + "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", +- "googleCalendarId": null, +- "customMessage": null, +- "xaiTemplate": "interview-30", ++ "calendarEventId": null, ++ "templateUrl": "interview-30", ++ "templateId": null, ++ "templateType": null, ++ "title": null, ++ "locationDetails": null, ++ "duration": null, + "round": 1, + "startTimestamp": null, +- "attendeesList": null, ++ "endTimestamp": null, ++ "hostName": null, ++ "hostEmail": null, ++ "guestNames": null, ++ "guestEmails": null, + "status": "Completed", ++ "rescheduleUrl": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:21:17.346Z", +- "updatedAt": "2021-05-09T21:21:17.346Z" ++ "updatedAt": "2021-05-09T21:21:17.346Z", ++ "deletedAt": null + } + ] + }, +@@ -2052,5 +2116,127 @@ + } + ] + } ++ ], ++ "Role": [ ++ { ++ "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.", ++ "listOfSkills": [ ++ "database", ++ "winforms", ++ "user interface (ui)", ++ "photoshop" ++ ], ++ "rates": [ ++ { ++ "global": 50, ++ "offShore": 10, ++ "inCountry": 20 ++ }, ++ { ++ "global": 25, ++ "offShore": 5, ++ "inCountry": 15 ++ } ++ ], ++ "numberOfMembers": "10", ++ "numberOfMembersAvailable": 8, ++ "imageUrl": "http://images.topcoder.com/member", ++ "timeToCandidate": 105, ++ "timeToInterview": 100, ++ "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", ++ "updatedBy": null, ++ "createdAt": "2021-05-27T21:43:08.201Z", ++ "updatedAt": "2021-05-27T21:43:08.201Z" ++ }, ++ { ++ "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.", ++ "listOfSkills": [ ++ "dropwizard", ++ "nginx", ++ "machine learning", ++ "force.com" ++ ], ++ "rates": [ ++ { ++ "global": 50, ++ "offShore": 10, ++ "inCountry": 20, ++ "rate20Global": 20, ++ "rate30Global": 20, ++ "rate20OffShore": 35, ++ "rate30OffShore": 35, ++ "rate20InCountry": 15, ++ "rate30InCountry": 15 ++ }, ++ { ++ "global": 25, ++ "offShore": 5, ++ "inCountry": 15, ++ "rate20Global": 20, ++ "rate30Global": 20, ++ "rate20OffShore": 35, ++ "rate30OffShore": 35, ++ "rate20InCountry": 15, ++ "rate30InCountry": 15 ++ } ++ ], ++ "numberOfMembers": "10", ++ "numberOfMembersAvailable": 8, ++ "imageUrl": "http://images.topcoder.com/member", ++ "timeToCandidate": 105, ++ "timeToInterview": 100, ++ "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", ++ "updatedBy": null, ++ "createdAt": "2021-05-27T21:43:04.717Z", ++ "updatedAt": "2021-05-27T21:43:04.717Z" ++ }, ++ { ++ "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.", ++ "listOfSkills": [ ++ "docker", ++ ".net", ++ "appcelerator", ++ "flux" ++ ], ++ "rates": [ ++ { ++ "global": 50, ++ "offShore": 10, ++ "inCountry": 20, ++ "rate20Global": 20, ++ "rate30Global": 20, ++ "rate20OffShore": 35, ++ "rate30OffShore": 35, ++ "rate20InCountry": 15, ++ "rate30InCountry": 15 ++ }, ++ { ++ "global": 25, ++ "offShore": 5, ++ "inCountry": 15, ++ "rate20Global": 20, ++ "rate30Global": 20, ++ "rate20OffShore": 35, ++ "rate30OffShore": 35, ++ "rate20InCountry": 15, ++ "rate30InCountry": 15 ++ } ++ ], ++ "numberOfMembers": "10", ++ "numberOfMembersAvailable": 6, ++ "imageUrl": "http://images.topcoder.com/member", ++ "timeToCandidate": 105, ++ "timeToInterview": 100, ++ "createdBy": "00000000-0000-0000-0000-000000000000", ++ "updatedBy": null, ++ "createdAt": "2021-05-27T21:43:09.342Z", ++ "updatedAt": "2021-05-27T21:43:09.342Z" ++ } + ] +-} ++} +\ No newline at end of file +diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json +index a0518c5..96250f3 100644 +--- a/docs/Topcoder-bookings-api.postman_collection.json ++++ b/docs/Topcoder-bookings-api.postman_collection.json +@@ -1,6 +1,6 @@ + { + "info": { +- "_postman_id": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", ++ "_postman_id": "b0508e11-af20-4ea3-bfda-fec9f40ea531", + "name": "Topcoder-bookings-api", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, +@@ -17816,6 +17816,2993 @@ + } + ] + }, ++ { ++ "name": "Roles", ++ "item": [ ++ { ++ "name": "Create Role", ++ "item": [ ++ { ++ "name": "create role with admin", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ " if(pm.response.status === \"OK\"){\r", ++ " const response = pm.response.json()\r", ++ " pm.environment.set(\"roleId-1\", response.id);\r", ++ " }\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n }\n ],\n \"numberOfMembers\": 10,\n \"numberOfMembersAvailable\": 8,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with booking manager", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ " if(pm.response.status === \"OK\"){\r", ++ " const response = pm.response.json()\r", ++ " pm.environment.set(\"roleId-2\", response.id);\r", ++ " }\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_bookingManager}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Angular Developer\",\n \"description\": \"Angular is an open-source, client-side framework based on TypeScript and designed for building web applications.\",\n \"listOfSkills\": [\n \"Database\",\n \"Winforms\",\n \"User Interface (Ui)\",\n \"Photoshop\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"numberOfMembersAvailable\": 8,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with m2m create", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ " if(pm.response.status === \"OK\"){\r", ++ " const response = pm.response.json()\r", ++ " pm.environment.set(\"roleId-3\", response.id);\r", ++ " }\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_m2m_create_role}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Salesforce Developer\",\n \"description\": \"A Salesforce developer is a programmer who builds Salesforce applications across various PaaS (Platform as a Service) platforms.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\",\n \"appcelerator\",\n \"Flux\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n }\n ],\n \"numberOfMembers\": 10,\n \"numberOfMembersAvailable\": 6,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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 are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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 are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid token", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 401', function () {\r", ++ " pm.response.to.have.status(401);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer invalid_token" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with existent name", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"Role: \\\"Dev Ops Engineer\\\" is already exists.\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with missing parameter 1", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.name\\\" is required\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with missing parameter 2", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.rates\\\" is required\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with missing parameter 3", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.rates\\\" does not contain 1 required value(s)\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with missing parameter 4", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.rates[0].global\\\" is required\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with missing parameter 5", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.rates[0].inCountry\\\" is required\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with missing parameter 6", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.rates[0].offShore\\\" is required\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 1", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.name\\\" length must be less than or equal to 50 characters long\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 2", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.listOfSkills[0]\\\" length must be less than or equal to 50 characters long\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard\",\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 3", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.listOfSkills\\\" must be an array\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\":\"Dropwizard\",\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 4", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.rates\\\" must be an array\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 5", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.rates[0].global\\\" must be a number\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": \"first\",\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 6", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.rates[0].inCountry\\\" must be a number\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": \"fifty\",\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 7", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.numberOfMembers\\\" must be a number\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": null,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 8", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.imageUrl\\\" must be a valid uri\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 9", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"role.timeToCandidate\\\" must be less than or equal to 32767\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 99999,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 10", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"skills: \\\"teamworking,communication,problem-solving\\\" are not valid\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer 2\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 55,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ }, ++ { ++ "name": "Get Role", ++ "item": [ ++ { ++ "name": "get role with admin", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "get role with booking manager fromDb", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_bookingManager}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-2}}?fromDb=true", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-2}}" ++ ], ++ "query": [ ++ { ++ "key": "fromDb", ++ "value": "true" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "get role with m2m read", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_m2m_read_role}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-3}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-3}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "get role with connect user fromDb", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}?fromDb=true", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ], ++ "query": [ ++ { ++ "key": "fromDb", ++ "value": "true" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "get role with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-2}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-2}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "get role with invalid token", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 401', function () {\r", ++ " pm.response.to.have.status(401);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer invalid token" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-2}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-2}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "get role with invalid id", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"id\\\" must be a valid GUID\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/invalid", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "invalid" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "get role with missing id", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 404', function () {\r", ++ " pm.response.to.have.status(404);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"id: 00000000-0000-0000-0000-000000000000 \\\"Role\\\" not found\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "00000000-0000-0000-0000-000000000000" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "search roles with admin", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "search roles with booking manager", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_bookingManager}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles?skillsList=dropwizard, nginx,, machine learning , FORce.com &keyword=ops e", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ], ++ "query": [ ++ { ++ "key": "skillsList", ++ "value": "dropwizard, nginx,, machine learning , FORce.com " ++ }, ++ { ++ "key": "keyword", ++ "value": "ops e" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "search roles with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles?skillsList=dataBase, ,Photoshop&keyword=sale", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ], ++ "query": [ ++ { ++ "key": "skillsList", ++ "value": "dataBase, ,Photoshop" ++ }, ++ { ++ "key": "keyword", ++ "value": "sale" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "search roles with m2m read", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_m2m_read_role}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles?skillsList=DOCKER,.NET&keyword=dev", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ], ++ "query": [ ++ { ++ "key": "skillsList", ++ "value": "DOCKER,.NET" ++ }, ++ { ++ "key": "keyword", ++ "value": "dev" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "search roles with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles?keyword=dev", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ], ++ "query": [ ++ { ++ "key": "keyword", ++ "value": "dev" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "search roles with invalid token", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 401', function () {\r", ++ " pm.response.to.have.status(401);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer invalid token" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ }, ++ { ++ "name": "Update Role", ++ "item": [ ++ { ++ "name": "update role with admin", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer edit\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with booking manager", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_bookingManager}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Angular Developer edit\",\n \"description\": \"Angular is an open-source, client-side framework based on TypeScript and designed for building web applications.\",\n \"listOfSkills\": [\n \"Database\",\n \"Winforms\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-2}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-2}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with m2m update", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_m2m_update_role}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Salesforce Developer edit\",\n \"description\": \"A Salesforce developer is a programmer who builds Salesforce applications across various PaaS (Platform as a Service) platforms.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-3}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-3}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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 are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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 are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid token", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 401', function () {\r", ++ " pm.response.to.have.status(401);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer invalid_token" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid id", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"id\\\" must be a valid GUID\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/invalid", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "invalid" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with missing id", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 404', function () {\r", ++ " pm.response.to.have.status(404);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"id: 00000000-0000-0000-0000-000000000000 \\\"Role\\\" doesn't exists.\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "00000000-0000-0000-0000-000000000000" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with existent name", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"Role: \\\"Angular Developer edit\\\" is already exists.\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Angular Developer edit\"\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 1", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"data.name\\\" length must be less than or equal to 50 characters long\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 2", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"data.listOfSkills[0]\\\" length must be less than or equal to 50 characters long\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking\",\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 3", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"data.listOfSkills\\\" must be an array\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\":\"Teamworking\",\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 4", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"data.rates\\\" must be an array\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 5", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"data.rates[0].global\\\" must be a number\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": \"first\",\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 6", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"data.rates[0].inCountry\\\" must be a number\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": \"fifty\",\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 7", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"data.numberOfMembers\\\" must be a number\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": \"hundred\",\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 8", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"data.imageUrl\\\" must be a valid uri\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 9", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"data.timeToCandidate\\\" must be less than or equal to 32767\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 99999,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 10", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"skills: \\\"teamworking\\\" are not valid\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 66,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ }, ++ { ++ "name": "Delete Role", ++ "item": [ ++ { ++ "name": "delete role with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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 are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "delete role with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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 are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "delete role with invalid token", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 401', function () {\r", ++ " pm.response.to.have.status(401);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer invalid_token" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"teamworking\",\n \"communication\",\n \"problem-solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "delete role with invalid id", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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(\"\\\"id\\\" must be a valid GUID\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/invalid", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "invalid" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "delete role with missing id", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 404', function () {\r", ++ " pm.response.to.have.status(404);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"id: 00000000-0000-0000-0000-000000000000 \\\"Role\\\" doesn't exists.\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "00000000-0000-0000-0000-000000000000" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "delete role with admin", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 204', function () {\r", ++ " pm.response.to.have.status(204);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "delete role with booking manager", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 204', function () {\r", ++ " pm.response.to.have.status(204);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_bookingManager}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-2}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-2}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "delete role with m2m delete", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 204', function () {\r", ++ " pm.response.to.have.status(204);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_m2m_delete_role}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-3}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-3}}" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ } ++ ] ++ }, + { + "name": "health check", + "item": [ +@@ -22399,7 +25386,227 @@ + ], + "body": { + "mode": "raw", +- "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", ++ "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_by_administrator}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "work-period-payments", ++ "{{workPeriodPaymentId_created_by_administrator}}" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ }, ++ { ++ "name": "Roles", ++ "item": [ ++ { ++ "name": "✔ create role with admin", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ " if(pm.response.status === \"OK\"){\r", ++ " const response = pm.response.json()\r", ++ " pm.environment.set(\"roleId-1\", response.id);\r", ++ " }\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ get role with admin", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ search roles with admin", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ update role with admin", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer edit\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ delete role with admin", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 204', function () {\r", ++ " pm.response.to.have.status(204);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", + "options": { + "raw": { + "language": "json" +@@ -22407,13 +25614,13 @@ + } + }, + "url": { +- "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_by_administrator}}", ++ "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ +- "work-period-payments", +- "{{workPeriodPaymentId_created_by_administrator}}" ++ "roles", ++ "{{roleId-1}}" + ] + } + }, +@@ -24635,12 +27842,295 @@ + { + "key": "Authorization", + "type": "text", +- "value": "Bearer {{token_member_tester1234}}" ++ "value": "Bearer {{token_member_tester1234}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_for_member}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "work-period-payments", ++ "{{workPeriodPaymentId_created_for_member}}" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ }, ++ { ++ "name": "Roles", ++ "item": [ ++ { ++ "name": "Before Start", ++ "item": [ ++ { ++ "name": "✔ create role with admin", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ " if(pm.response.status === \"OK\"){\r", ++ " const response = pm.response.json()\r", ++ " pm.environment.set(\"roleId-1\", response.id);\r", ++ " }\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ }, ++ { ++ "name": "✘ create role with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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 are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ get role with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ search roles with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles?keyword=Dev", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ], ++ "query": [ ++ { ++ "key": "keyword", ++ "value": "Dev" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✘ update role with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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 are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✘ delete role with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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 are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" + } + ], + "body": { + "mode": "raw", +- "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", ++ "raw": "", + "options": { + "raw": { + "language": "json" +@@ -24648,13 +28138,13 @@ + } + }, + "url": { +- "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_for_member}}", ++ "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ +- "work-period-payments", +- "{{workPeriodPaymentId_created_for_member}}" ++ "roles", ++ "{{roleId-1}}" + ] + } + }, +@@ -26894,10 +30384,297 @@ + "response": [] + } + ] ++ }, ++ { ++ "name": "Roles", ++ "item": [ ++ { ++ "name": "Before Start", ++ "item": [ ++ { ++ "name": "✔ create role with admin", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ " if(pm.response.status === \"OK\"){\r", ++ " const response = pm.response.json()\r", ++ " pm.environment.set(\"roleId-1\", response.id);\r", ++ " }\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer 2\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ }, ++ { ++ "name": "✘ create role with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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 are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ get role with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ search roles with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 200', function () {\r", ++ " pm.response.to.have.status(200);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles?skillsList=Dropwizard, ,NGINX&keyword=Dev", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ], ++ "query": [ ++ { ++ "key": "skillsList", ++ "value": "Dropwizard, ,NGINX" ++ }, ++ { ++ "key": "keyword", ++ "value": "Dev" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✘ update role with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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 are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✘ delete role with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "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 are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] + } + ] + } + ] + } + ] +-} ++} +\ No newline at end of file +diff --git a/docs/swagger.yaml b/docs/swagger.yaml +index a0b6064..e5f1ac2 100644 +--- a/docs/swagger.yaml ++++ b/docs/swagger.yaml +@@ -18,6 +18,8 @@ tags: + - name: ResourceBookings + - name: Teams + - name: WorkPeriods ++ - name: WorkPeriodPayments ++ - name: Roles + paths: + /jobs: + post: +@@ -3245,6 +3247,267 @@ paths: + application/json: + schema: + $ref: "#/components/schemas/Error" ++ /roles/new: ++ post: ++ tags: ++ - Roles ++ description: | ++ Create Role. ++ ++ **Authorization** Topcoder m2m token with create scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. ++ security: ++ - bearerAuth: [] ++ requestBody: ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/RoleRequestBody" ++ responses: ++ "200": ++ description: OK ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Role" ++ "400": ++ description: Bad request ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "401": ++ description: Not authenticated ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "403": ++ description: Forbidden ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "500": ++ description: Internal Server Error ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ /roles: ++ get: ++ tags: ++ - Roles ++ description: | ++ Search roles. ++ ++ **Authorization** Topcoder m2m token with read scope is allowed. Topcoder user token with any role is allowed. ++ security: ++ - bearerAuth: [] ++ parameters: ++ - in: query ++ name: skillsList ++ required: false ++ schema: ++ type: string ++ description: comma separated skill names. case-insensitive. ++ - in: query ++ name: keyword ++ required: false ++ schema: ++ type: string ++ description: role name. case-insensitive. partial match allowed ++ responses: ++ "200": ++ description: OK ++ content: ++ application/json: ++ schema: ++ type: array ++ items: ++ $ref: "#/components/schemas/Role" ++ "400": ++ description: Bad request ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "401": ++ description: Not authenticated ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "500": ++ description: Internal Server Error ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ /roles/{id}: ++ get: ++ tags: ++ - Roles ++ description: | ++ Get role by id. ++ ++ **Authorization** Topcoder m2m token with read scope is allowed. Topcoder user token with any role is allowed. ++ security: ++ - bearerAuth: [] ++ parameters: ++ - in: path ++ name: id ++ description: The role id. ++ required: true ++ schema: ++ type: string ++ format: uuid ++ - in: query ++ name: fromDb ++ description: get data from db or not. ++ required: false ++ schema: ++ type: boolean ++ default: false ++ responses: ++ "200": ++ description: OK ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Role" ++ "400": ++ description: Bad request ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "401": ++ description: Not authenticated ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "404": ++ description: Not Found ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "500": ++ description: Internal Server Error ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ patch: ++ tags: ++ - Roles ++ description: | ++ Partial Update role. ++ ++ **Authorization** Topcoder m2m token with update scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. ++ security: ++ - bearerAuth: [] ++ parameters: ++ - in: path ++ name: id ++ description: The id of role. ++ required: true ++ schema: ++ type: string ++ format: uuid ++ requestBody: ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/RolePatchRequestBody" ++ responses: ++ "200": ++ description: OK ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Role" ++ "400": ++ description: Bad request ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "401": ++ description: Not authenticated ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "403": ++ description: Forbidden ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "404": ++ description: Not Found ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "500": ++ description: Internal Server Error ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ delete: ++ tags: ++ - Roles ++ description: | ++ Delete the role. ++ ++ **Authorization** Topcoder m2m token with delete scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. ++ security: ++ - bearerAuth: [] ++ parameters: ++ - in: path ++ name: id ++ description: The id of role. ++ required: true ++ schema: ++ type: string ++ format: uuid ++ responses: ++ "204": ++ description: OK ++ "400": ++ description: Bad request ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "401": ++ description: Not authenticated ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "403": ++ description: Forbidden ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "404": ++ description: Not Found ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "500": ++ description: Internal Server Error ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" + /health: + get: + tags: +@@ -3335,6 +3598,13 @@ components: + type: string + format: uuid + description: "The skill id." ++ roleIds: ++ type: array ++ description: "The roles." ++ items: ++ type: string ++ format: uuid ++ description: "The role id." + status: + type: string + enum: ["sourcing", "in-review", "assigned", "closed", "cancelled"] +@@ -3424,6 +3694,13 @@ components: + type: string + format: uuid + description: "The skill id." ++ roleIds: ++ type: array ++ description: "The roles." ++ items: ++ type: string ++ format: uuid ++ description: "The role id." + isApplicationPageActive: + type: boolean + default: false +@@ -3865,6 +4142,13 @@ components: + type: string + format: uuid + description: "The skill id." ++ roleIds: ++ type: array ++ description: "The roles." ++ items: ++ type: string ++ format: uuid ++ description: "The role id." + isApplicationPageActive: + type: boolean + default: false +@@ -4710,6 +4994,198 @@ components: + type: string + description: "the email of a member" + example: "xxx@xxx.com" ++ Role: ++ required: ++ - id ++ - name ++ - rates ++ - createdAt ++ - createdBy ++ properties: ++ id: ++ type: string ++ format: uuid ++ description: "The role id." ++ name: ++ type: string ++ example: "Dev Ops Engineer" ++ description: "The role name." ++ description: ++ type: string ++ example: "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: "The role description" ++ listOfSkills: ++ type: array ++ description: "The array of skill names." ++ items: ++ type: string ++ example: "HTML" ++ description: "The skill name" ++ rates: ++ type: array ++ description: "The rates object array." ++ items: ++ $ref: "#/components/schemas/RoleRates" ++ numberOfMembers: ++ type: number ++ example: 100 ++ description: "The number of members." ++ numberOfMembersAvailable: ++ type: integer ++ example: 100 ++ description: "The number of members available." ++ imageUrl: ++ type: string ++ format: url ++ example: "http://images.topcoder.com/images" ++ description: "The image url of the role." ++ timeToCandidate: ++ type: integer ++ example: 200 ++ description: "The time to candidate." ++ timeToInterview: ++ type: integer ++ example: 300 ++ description: "The time to interview." ++ createdAt: ++ type: string ++ format: date-time ++ description: "The role created date." ++ createdBy: ++ type: string ++ format: uuid ++ description: "The user Id who created the role.(Will get the user info from the token)" ++ updatedAt: ++ type: string ++ format: date-time ++ description: "The role last updated at." ++ updatedBy: ++ type: string ++ format: uuid ++ description: "The user Id who updated the role last time.(Will get the user info from the token)" ++ RoleRequestBody: ++ required: ++ - name ++ - rates ++ properties: ++ name: ++ type: string ++ example: "Dev Ops Engineer" ++ description: "The role name." ++ description: ++ type: string ++ example: "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: "The role description" ++ listOfSkills: ++ type: array ++ description: "The array of skill names." ++ items: ++ type: string ++ example: "HTML" ++ description: "The skill name" ++ rates: ++ type: array ++ description: "The rates object array." ++ items: ++ $ref: "#/components/schemas/RoleRates" ++ numberOfMembers: ++ type: number ++ example: 100 ++ description: "The number of members." ++ numberOfMembersAvailable: ++ type: number ++ example: 100 ++ description: "The number of members available." ++ imageUrl: ++ type: string ++ format: url ++ example: "http://images.topcoder.com/images" ++ description: "The image url of the role." ++ timeToCandidate: ++ type: integer ++ example: 200 ++ description: "The time to candidate." ++ timeToInterview: ++ type: integer ++ example: 300 ++ description: "The time to interview." ++ RolePatchRequestBody: ++ properties: ++ name: ++ type: string ++ example: "Dev Ops Engineer" ++ description: "The role name." ++ description: ++ type: string ++ example: "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: "The role description" ++ listOfSkills: ++ type: array ++ description: "The array of skill names." ++ items: ++ type: string ++ example: "HTML" ++ description: "The skill name" ++ rates: ++ type: array ++ description: "The rates object array." ++ items: ++ $ref: "#/components/schemas/RoleRates" ++ numberOfMembers: ++ type: number ++ example: 100 ++ description: "The number of members." ++ numberOfMembersAvailable: ++ type: number ++ example: 100 ++ description: "The number of members available." ++ imageUrl: ++ type: string ++ format: url ++ example: "http://images.topcoder.com/images" ++ description: "The image url of the role." ++ timeToCandidate: ++ type: integer ++ example: 200 ++ description: "The time to candidate." ++ timeToInterview: ++ type: integer ++ example: 300 ++ description: "The time to interview." ++ RoleRates: ++ required: ++ - global ++ - inCountry ++ - offShore ++ type: object ++ properties: ++ global: ++ type: integer ++ example: 10 ++ inCountry: ++ type: integer ++ example: 20 ++ offShore: ++ type: integer ++ example: 30 ++ rate30Global: ++ type: integer ++ example: 10 ++ rate30InCountry: ++ type: integer ++ example: 20 ++ rate30OffShore: ++ type: integer ++ example: 30 ++ rate20Global: ++ type: integer ++ example: 10 ++ rate20InCountry: ++ type: integer ++ example: 20 ++ rate20OffShore: ++ type: integer ++ example: 30 + ProjectMember: + type: object + example: +diff --git a/docs/topcoder-bookings.postman_environment.json b/docs/topcoder-bookings.postman_environment.json +index 837b55d..c83fc9a 100644 +--- a/docs/topcoder-bookings.postman_environment.json ++++ b/docs/topcoder-bookings.postman_environment.json +@@ -1,5 +1,5 @@ + { +- "id": "228f4dcc-6914-462e-9b56-3285b643a2f8", ++ "id": "0ce42def-1c70-4c24-8986-914caa57f3c8", + "name": "topcoder-bookings", + "values": [ + { +@@ -312,11 +312,6 @@ + "value": "", + "enabled": true + }, +- { +- "key": "job_id_created_for_member", +- "value": "", +- "enabled": true +- }, + { + "key": "resource_bookings_id_created_for_member", + "value": "", +@@ -327,11 +322,6 @@ + "value": "", + "enabled": true + }, +- { +- "key": "job_id_created_for_connect_manager", +- "value": "", +- "enabled": true +- }, + { + "key": "resource_bookings_id_created_for_connect_manager", + "value": "", +@@ -461,9 +451,49 @@ + "key": "interview_id_created_for_connect_manager", + "value": "", + "enabled": true ++ }, ++ { ++ "key": "token_m2m_create_role", ++ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.f1QP1QTacyDxy7dwzUhBIT8blXCjKn_mnu9Cg59vIc8", ++ "enabled": true ++ }, ++ { ++ "key": "token_m2m_read_role", ++ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJyZWFkOnRhYXMtcm9sZXMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.ZeWS_W2o8YwlvIB_-z0CFFa9zhRjptCk7qNXsPPWxVY", ++ "enabled": true ++ }, ++ { ++ "key": "token_m2m_update_role", ++ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJ1cGRhdGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.0t4k0skZmxAUKuHQrG3ZrO2dgWcDMLD8W1rVluCy7XQ", ++ "enabled": true ++ }, ++ { ++ "key": "token_m2m_delete_role", ++ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJkZWxldGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.NSBbWOk5jCB8nIvLiZwJtR9px5wmUQaQjgpDlMDJ9hk", ++ "enabled": true ++ }, ++ { ++ "key": "token_m2m_all_role", ++ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJhbGw6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.cn0QVTOFnbHJckYqmGcpUBT8wQUxXWwtteWU7uhlDtI", ++ "enabled": true ++ }, ++ { ++ "key": "roleId-1", ++ "value": "", ++ "enabled": true ++ }, ++ { ++ "key": "roleId-2", ++ "value": "", ++ "enabled": true ++ }, ++ { ++ "key": "roleId-3", ++ "value": "", ++ "enabled": true + } + ], + "_postman_variable_scope": "environment", +- "_postman_exported_at": "2021-05-10T05:06:38.661Z", +- "_postman_exported_using": "Postman/8.3.1" ++ "_postman_exported_at": "2021-05-27T01:32:45.726Z", ++ "_postman_exported_using": "Postman/8.5.1" + } +\ No newline at end of file +diff --git a/local/kafka-client/topics.txt b/local/kafka-client/topics.txt +index 8766a1b..760c3a8 100644 +--- a/local/kafka-client/topics.txt ++++ b/local/kafka-client/topics.txt +@@ -3,16 +3,19 @@ taas.jobcandidate.create + taas.resourcebooking.create + taas.workperiod.create + taas.workperiodpayment.create ++taas.role.requested + taas.job.update + taas.jobcandidate.update + taas.resourcebooking.update + taas.workperiod.update + taas.workperiodpayment.update ++taas.role.update + taas.job.delete + taas.jobcandidate.delete + taas.resourcebooking.delete + taas.workperiod.delete + taas.workperiodpayment.delete ++taas.role.delete + taas.interview.requested + taas.interview.update + taas.interview.bulkUpdate +diff --git a/migrations/2021-05-27-1-role-table-create.js b/migrations/2021-05-27-1-role-table-create.js +new file mode 100644 +index 0000000..bce2ae1 +--- /dev/null ++++ b/migrations/2021-05-27-1-role-table-create.js +@@ -0,0 +1,146 @@ ++const config = require('config') ++ ++/* ++ * Create role table ++ */ ++ ++module.exports = { ++ up: async (queryInterface, Sequelize) => { ++ const transaction = await queryInterface.sequelize.transaction() ++ try { ++ await queryInterface.createTable('roles', { ++ id: { ++ type: Sequelize.UUID, ++ primaryKey: true, ++ allowNull: false, ++ defaultValue: Sequelize.UUIDV4 ++ }, ++ name: { ++ type: Sequelize.STRING(50), ++ allowNull: false ++ }, ++ description: { ++ type: Sequelize.STRING(1000) ++ }, ++ listOfSkills: { ++ field: 'list_of_skills', ++ type: Sequelize.ARRAY({ ++ type: Sequelize.STRING(50) ++ }) ++ }, ++ rates: { ++ type: Sequelize.ARRAY({ ++ type: Sequelize.JSONB({ ++ global: { ++ type: Sequelize.SMALLINT, ++ allowNull: false ++ }, ++ inCountry: { ++ field: 'in_country', ++ type: Sequelize.SMALLINT, ++ allowNull: false ++ }, ++ offShore: { ++ field: 'off_shore', ++ type: Sequelize.SMALLINT, ++ allowNull: false ++ }, ++ rate30Global: { ++ field: 'rate30_global', ++ type: Sequelize.SMALLINT ++ }, ++ rate30InCountry: { ++ field: 'rate30_in_country', ++ type: Sequelize.SMALLINT ++ }, ++ rate30OffShore: { ++ field: 'rate30_off_shore', ++ type: Sequelize.SMALLINT ++ }, ++ rate20Global: { ++ field: 'rate20_global', ++ type: Sequelize.SMALLINT ++ }, ++ rate20InCountry: { ++ field: 'rate20_in_country', ++ type: Sequelize.SMALLINT ++ }, ++ rate20OffShore: { ++ field: 'rate20_off_shore', ++ type: Sequelize.SMALLINT ++ } ++ }), ++ allowNull: false ++ }), ++ allowNull: false ++ }, ++ numberOfMembers: { ++ field: 'number_of_members', ++ type: Sequelize.NUMERIC ++ }, ++ numberOfMembersAvailable: { ++ field: 'number_of_members_available', ++ type: Sequelize.SMALLINT ++ }, ++ imageUrl: { ++ field: 'image_url', ++ type: Sequelize.STRING(255) ++ }, ++ timeToCandidate: { ++ field: 'time_to_candidate', ++ type: Sequelize.SMALLINT ++ }, ++ timeToInterview: { ++ field: 'time_to_interview', ++ type: Sequelize.SMALLINT ++ }, ++ createdBy: { ++ field: 'created_by', ++ type: Sequelize.UUID, ++ allowNull: false ++ }, ++ updatedBy: { ++ field: 'updated_by', ++ type: Sequelize.UUID ++ }, ++ createdAt: { ++ field: 'created_at', ++ type: Sequelize.DATE ++ }, ++ updatedAt: { ++ field: 'updated_at', ++ type: Sequelize.DATE ++ }, ++ deletedAt: { ++ field: 'deleted_at', ++ type: Sequelize.DATE ++ } ++ }, { ++ schema: config.DB_SCHEMA_NAME, ++ transaction ++ }) ++ await queryInterface.addIndex( ++ { ++ tableName: 'roles', ++ schema: config.DB_SCHEMA_NAME ++ }, ++ ['name'], ++ { ++ type: 'UNIQUE', ++ where: { deleted_at: null }, ++ transaction: transaction ++ } ++ ) ++ await transaction.commit() ++ } catch (err) { ++ await transaction.rollback() ++ throw err ++ } ++ }, ++ down: async (queryInterface, Sequelize) => { ++ await queryInterface.dropTable({ ++ tableName: 'roles', ++ schema: config.DB_SCHEMA_NAME ++ }) ++ } ++} +diff --git a/migrations/2021-05-27-2-job-add-roleIds-field.js b/migrations/2021-05-27-2-job-add-roleIds-field.js +new file mode 100644 +index 0000000..a5b9f4b +--- /dev/null ++++ b/migrations/2021-05-27-2-job-add-roleIds-field.js +@@ -0,0 +1,19 @@ ++const config = require('config') ++ ++/* ++ * Add roleIds field to the Job model. ++ */ ++ ++module.exports = { ++ up: async (queryInterface, Sequelize) => { ++ await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'role_ids', ++ { ++ type: Sequelize.ARRAY({ ++ type: Sequelize.UUID ++ }) ++ }) ++ }, ++ down: async (queryInterface, Sequelize) => { ++ await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'role_ids') ++ } ++} +diff --git a/package.json b/package.json +index 0fa24cc..510504f 100644 +--- a/package.json ++++ b/package.json +@@ -15,6 +15,7 @@ + "index:jobs": "node scripts/es/reIndexJobs.js", + "index:job-candidates": "node scripts/es/reIndexJobCandidates.js", + "index:resource-bookings": "node scripts/es/reIndexResourceBookings.js", ++ "index:roles": "node scripts/es/reIndexRoles.js", + "data:export": "node scripts/data/exportData.js", + "data:import": "node scripts/data/importData.js", + "migrate": "npx sequelize db:migrate", +diff --git a/scripts/data/exportData.js b/scripts/data/exportData.js +index 4eee1ad..cb61e58 100644 +--- a/scripts/data/exportData.js ++++ b/scripts/data/exportData.js +@@ -28,7 +28,7 @@ const resourceBookingModelOpts = { + + const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH + const userPrompt = `WARNING: are you sure you want to export all data in the database to a json file with the path ${filePath}? This will overwrite the file.` +-const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts] ++const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts, 'Role'] + + async function exportData () { + await helper.promptUser(userPrompt, async () => { +diff --git a/scripts/data/importData.js b/scripts/data/importData.js +index 2e9c168..a0aeeb6 100644 +--- a/scripts/data/importData.js ++++ b/scripts/data/importData.js +@@ -28,7 +28,7 @@ const resourceBookingModelOpts = { + + const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH + const userPrompt = `WARNING: this would remove existing data. Are you sure you want to import data from a json file with the path ${filePath}?` +-const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts] ++const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts, 'Role'] + + async function importData () { + await helper.promptUser(userPrompt, async () => { +diff --git a/scripts/es/createIndex.js b/scripts/es/createIndex.js +index d2c7294..269cd5a 100644 +--- a/scripts/es/createIndex.js ++++ b/scripts/es/createIndex.js +@@ -8,7 +8,8 @@ const helper = require('../../src/common/helper') + const indices = [ + config.get('esConfig.ES_INDEX_JOB'), + config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), +- config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') ++ config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), ++ config.get('esConfig.ES_INDEX_ROLE') + ] + const userPrompt = `WARNING: Are you sure want to create the following elasticsearch indices: ${indices}?` + +diff --git a/scripts/es/deleteIndex.js b/scripts/es/deleteIndex.js +index 6e30995..724d355 100644 +--- a/scripts/es/deleteIndex.js ++++ b/scripts/es/deleteIndex.js +@@ -8,7 +8,8 @@ const helper = require('../../src/common/helper') + const indices = [ + config.get('esConfig.ES_INDEX_JOB'), + config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), +- config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') ++ config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), ++ config.get('esConfig.ES_INDEX_ROLE') + ] + const userPrompt = `WARNING: this would remove existent data! Are you sure want to delete the following eleasticsearch indices: ${indices}?` + +diff --git a/scripts/es/reIndexAll.js b/scripts/es/reIndexAll.js +index 802695d..0367be1 100644 +--- a/scripts/es/reIndexAll.js ++++ b/scripts/es/reIndexAll.js +@@ -34,6 +34,7 @@ async function indexAll () { + await helper.indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) + await helper.indexBulkDataToES(jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger) + await helper.indexBulkDataToES(resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger) ++ await helper.indexBulkDataToES('Role', config.get('esConfig.ES_INDEX_ROLE'), logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'indexAll' }) +diff --git a/scripts/es/reIndexRoles.js b/scripts/es/reIndexRoles.js +new file mode 100644 +index 0000000..a4507aa +--- /dev/null ++++ b/scripts/es/reIndexRoles.js +@@ -0,0 +1,37 @@ ++/** ++ * Reindex Roles data in Elasticsearch using data from database ++ */ ++const config = require('config') ++const logger = require('../../src/common/logger') ++const helper = require('../../src/common/helper') ++ ++const roleId = helper.getParamFromCliArgs() ++const index = config.get('esConfig.ES_INDEX_ROLE') ++const reIndexAllRolesPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the index ${index}?` ++const reIndexRolePrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the document with id ${roleId} in index ${index}?` ++ ++async function reIndexRoles () { ++ if (roleId === null) { ++ await helper.promptUser(reIndexAllRolesPrompt, async () => { ++ try { ++ await helper.indexBulkDataToES('Role', index, logger) ++ process.exit(0) ++ } catch (err) { ++ logger.logFullError(err, { component: 'reIndexRoles' }) ++ process.exit(1) ++ } ++ }) ++ } else { ++ await helper.promptUser(reIndexRolePrompt, async () => { ++ try { ++ await helper.indexDataToEsById(roleId, 'Role', index, logger) ++ process.exit(0) ++ } catch (err) { ++ logger.logFullError(err, { component: 'reIndexRoles' }) ++ process.exit(1) ++ } ++ }) ++ } ++} ++ ++reIndexRoles() +diff --git a/src/bootstrap.js b/src/bootstrap.js +index 2999f13..896e6c9 100644 +--- a/src/bootstrap.js ++++ b/src/bootstrap.js +@@ -16,7 +16,7 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') + Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') + Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') + Joi.workload = () => Joi.string().valid('full-time', 'fractional') +-Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed') ++Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed') + Joi.title = () => Joi.string().max(128) + Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') + Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) +@@ -26,6 +26,7 @@ Joi.workPeriodPaymentStatus = () => Joi.string().valid('completed', 'cancelled') + // See https://joi.dev/api/?v=17.3.0#string fro details why it's like this. + // In many cases we would like to allow empty string to make it easier to create UI for editing data. + Joi.stringAllowEmpty = () => Joi.string().allow('') ++Joi.smallint = () => Joi.number().min(-32768).max(32767) + + function buildServices (dir) { + const files = fs.readdirSync(dir) +diff --git a/src/common/helper.js b/src/common/helper.js +index 0ce1190..66cf32d 100644 +--- a/src/common/helper.js ++++ b/src/common/helper.js +@@ -2,50 +2,50 @@ + * This file defines helper methods + */ + +-const fs = require('fs'); +-const querystring = require('querystring'); +-const Confirm = require('prompt-confirm'); +-const Bottleneck = require('bottleneck'); +-const AWS = require('aws-sdk'); +-const config = require('config'); +-const HttpStatus = require('http-status-codes'); +-const _ = require('lodash'); +-const request = require('superagent'); +-const elasticsearch = require('@elastic/elasticsearch'); ++const fs = require('fs') ++const querystring = require('querystring') ++const Confirm = require('prompt-confirm') ++const Bottleneck = require('bottleneck') ++const AWS = require('aws-sdk') ++const config = require('config') ++const HttpStatus = require('http-status-codes') ++const _ = require('lodash') ++const request = require('superagent') ++const elasticsearch = require('@elastic/elasticsearch') + const { +- ResponseError: ESResponseError, +-} = require('@elastic/elasticsearch/lib/errors'); +-const errors = require('../common/errors'); +-const logger = require('./logger'); +-const models = require('../models'); +-const eventDispatcher = require('./eventDispatcher'); +-const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper'); +-const moment = require('moment'); ++ ResponseError: ESResponseError ++} = require('@elastic/elasticsearch/lib/errors') ++const errors = require('../common/errors') ++const logger = require('./logger') ++const models = require('../models') ++const eventDispatcher = require('./eventDispatcher') ++const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') ++const moment = require('moment') + + const localLogger = { + debug: (message) => + logger.debug({ + component: 'helper', + context: message.context, +- message: message.message, ++ message: message.message + }), + error: (message) => + logger.error({ + component: 'helper', + context: message.context, +- message: message.message, ++ message: message.message + }), + info: (message) => + logger.info({ + component: 'helper', + context: message.context, +- message: message.message, +- }), +-}; ++ message: message.message ++ }) ++} + +-AWS.config.region = config.esConfig.AWS_REGION; ++AWS.config.region = config.esConfig.AWS_REGION + +-const m2mAuth = require('tc-core-library-js').auth.m2m; ++const m2mAuth = require('tc-core-library-js').auth.m2m + + const m2m = m2mAuth( + _.pick(config, [ +@@ -53,9 +53,9 @@ const m2m = m2mAuth( + 'AUTH0_AUDIENCE', + 'AUTH0_CLIENT_ID', + 'AUTH0_CLIENT_SECRET', +- 'AUTH0_PROXY_SERVER_URL', ++ 'AUTH0_PROXY_SERVER_URL' + ]) +-); ++) + + const m2mForUbahn = m2mAuth({ + AUTH0_AUDIENCE: config.AUTH0_AUDIENCE_UBAHN, +@@ -64,20 +64,20 @@ const m2mForUbahn = m2mAuth({ + 'TOKEN_CACHE_TIME', + 'AUTH0_CLIENT_ID', + 'AUTH0_CLIENT_SECRET', +- 'AUTH0_PROXY_SERVER_URL', +- ]), +-}); ++ 'AUTH0_PROXY_SERVER_URL' ++ ]) ++}) + +-let busApiClient; ++let busApiClient + + /** + * Get bus api client. + * + * @returns {Object} the bus api client + */ +-function getBusApiClient() { ++function getBusApiClient () { + if (busApiClient) { +- return busApiClient; ++ return busApiClient + } + busApiClient = busApi( + _.pick(config, [ +@@ -88,17 +88,17 @@ function getBusApiClient() { + 'AUTH0_CLIENT_SECRET', + 'BUSAPI_URL', + 'KAFKA_ERROR_TOPIC', +- 'AUTH0_PROXY_SERVER_URL', ++ 'AUTH0_PROXY_SERVER_URL' + ]) +- ); +- return busApiClient; ++ ) ++ return busApiClient + } + + // ES Client mapping +-const esClients = {}; ++const esClients = {} + + // The es index property mapping +-const esIndexPropertyMapping = {}; ++const esIndexPropertyMapping = {} + esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { + projectId: { type: 'integer' }, + externalId: { type: 'keyword' }, +@@ -113,11 +113,12 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { + skills: { type: 'keyword' }, + status: { type: 'keyword' }, + isApplicationPageActive: { type: 'boolean' }, ++ roleIds: { type: 'keyword' }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, +- updatedBy: { type: 'keyword' }, +-}; ++ updatedBy: { type: 'keyword' } ++} + esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { + jobId: { type: 'keyword' }, + userId: { type: 'keyword' }, +@@ -150,14 +151,14 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, + updatedBy: { type: 'keyword' }, +- deletedAt: { type: 'date' }, +- }, ++ deletedAt: { type: 'date' } ++ } + }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, +- updatedBy: { type: 'keyword' }, +-}; ++ updatedBy: { type: 'keyword' } ++} + esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { + projectId: { type: 'integer' }, + userId: { type: 'keyword' }, +@@ -195,32 +196,59 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, +- updatedBy: { type: 'keyword' }, +- }, ++ updatedBy: { type: 'keyword' } ++ } + }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, +- updatedBy: { type: 'keyword' }, +- }, ++ updatedBy: { type: 'keyword' } ++ } ++ }, ++ createdAt: { type: 'date' }, ++ createdBy: { type: 'keyword' }, ++ updatedAt: { type: 'date' }, ++ updatedBy: { type: 'keyword' } ++} ++esIndexPropertyMapping[config.get('esConfig.ES_INDEX_ROLE')] = { ++ name: { type: 'keyword' }, ++ description: { type: 'keyword' }, ++ listOfSkills: { type: 'keyword' }, ++ rates: { ++ properties: { ++ global: { type: 'integer' }, ++ inCountry: { type: 'integer' }, ++ offShore: { type: 'integer' }, ++ rate30Global: { type: 'integer' }, ++ rate30InCountry: { type: 'integer' }, ++ rate30OffShore: { type: 'integer' }, ++ rate20Global: { type: 'integer' }, ++ rate20InCountry: { type: 'integer' }, ++ rate20OffShore: { type: 'integer' } ++ } + }, ++ numberOfMembers: { type: 'integer' }, ++ numberOfMembersAvailable: { type: 'integer' }, ++ imageUrl: { type: 'keyword' }, ++ timeToCandidate: { type: 'integer' }, ++ timeToInterview: { type: 'integer' }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, +- updatedBy: { type: 'keyword' }, +-}; ++ updatedBy: { type: 'keyword' } ++} + + /** + * Get the first parameter from cli arguments + */ +-function getParamFromCliArgs() { +- const filteredArgs = process.argv.filter((arg) => !arg.includes('--')); ++function getParamFromCliArgs () { ++ const filteredArgs = process.argv.filter((arg) => !arg.includes('--')) + + if (filteredArgs.length > 2) { +- return filteredArgs[2]; ++ return filteredArgs[2] + } + +- return null; ++ return null + } + + /** +@@ -228,18 +256,18 @@ function getParamFromCliArgs() { + * @param {string} promptQuery the query to ask the user + * @param {function} cb the callback function + */ +-async function promptUser(promptQuery, cb) { ++async function promptUser (promptQuery, cb) { + if (process.argv.includes('--force')) { +- await cb(); +- return; ++ await cb() ++ return + } + +- const prompt = new Confirm(promptQuery); ++ const prompt = new Confirm(promptQuery) + prompt.ask(async (answer) => { + if (answer) { +- await cb(); ++ await cb() + } +- }); ++ }) + } + + /** +@@ -248,23 +276,23 @@ async function promptUser(promptQuery, cb) { + * @param {Object} logger the logger object + * @param {Object} esClient the elasticsearch client (optional, will create if not given) + */ +-async function createIndex(index, logger, esClient = null) { ++async function createIndex (index, logger, esClient = null) { + if (!esClient) { +- esClient = getESClient(); ++ esClient = getESClient() + } + + await esClient.indices.create({ + index, + body: { + mappings: { +- properties: esIndexPropertyMapping[index], +- }, +- }, +- }); ++ properties: esIndexPropertyMapping[index] ++ } ++ } ++ }) + logger.info({ + component: 'createIndex', +- message: `ES Index ${index} creation succeeded!`, +- }); ++ message: `ES Index ${index} creation succeeded!` ++ }) + } + + /** +@@ -273,45 +301,45 @@ async function createIndex(index, logger, esClient = null) { + * @param {Object} logger the logger object + * @param {Object} esClient the elasticsearch client (optional, will create if not given) + */ +-async function deleteIndex(index, logger, esClient = null) { ++async function deleteIndex (index, logger, esClient = null) { + if (!esClient) { +- esClient = getESClient(); ++ esClient = getESClient() + } + +- await esClient.indices.delete({ index }); ++ await esClient.indices.delete({ index }) + logger.info({ + component: 'deleteIndex', +- message: `ES Index ${index} deletion succeeded!`, +- }); ++ message: `ES Index ${index} deletion succeeded!` ++ }) + } + + /** + * Split data into bulks + * @param {Array} data the array of data to split + */ +-function getBulksFromDocuments(data) { +- const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6; +- const bulks = []; +- let documentIndex = 0; +- let currentBulkSize = 0; +- let currentBulk = []; ++function getBulksFromDocuments (data) { ++ const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6 ++ const bulks = [] ++ let documentIndex = 0 ++ let currentBulkSize = 0 ++ let currentBulk = [] + + while (true) { + // break loop when parsed all documents + if (documentIndex >= data.length) { +- bulks.push(currentBulk); +- break; ++ bulks.push(currentBulk) ++ break + } + + // check if current document size is greater than the max bulk size, if so, throw error + const currentDocumentSize = Buffer.byteLength( + JSON.stringify(data[documentIndex]), + 'utf-8' +- ); ++ ) + if (maxBytes < currentDocumentSize) { + throw new Error( + `Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.` +- ); ++ ) + } + + if ( +@@ -320,17 +348,17 @@ function getBulksFromDocuments(data) { + ) { + // if adding the current document goes over the max bulk size OR goes over max number of docs + // then push the current bulk to bulks array and reset the current bulk +- bulks.push(currentBulk); +- currentBulk = []; +- currentBulkSize = 0; ++ bulks.push(currentBulk) ++ currentBulk = [] ++ currentBulkSize = 0 + } else { + // otherwise, add document to current bulk +- currentBulk.push(data[documentIndex]); +- currentBulkSize += currentDocumentSize; +- documentIndex++; ++ currentBulk.push(data[documentIndex]) ++ currentBulkSize += currentDocumentSize ++ documentIndex++ + } + } +- return bulks; ++ return bulks + } + + /** +@@ -339,57 +367,57 @@ function getBulksFromDocuments(data) { + * @param {Object} indexName the index name + * @param {Object} logger the logger object + */ +-async function indexBulkDataToES(modelOpts, indexName, logger) { +- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; +- const include = _.get(modelOpts, 'include', []); ++async function indexBulkDataToES (modelOpts, indexName, logger) { ++ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName ++ const include = _.get(modelOpts, 'include', []) + + logger.info({ + component: 'indexBulkDataToES', +- message: `Reindexing of ${modelName}s started!`, +- }); ++ message: `Reindexing of ${modelName}s started!` ++ }) + +- const esClient = getESClient(); ++ const esClient = getESClient() + + // clear index +- const indexExistsRes = await esClient.indices.exists({ index: indexName }); ++ const indexExistsRes = await esClient.indices.exists({ index: indexName }) + if (indexExistsRes.statusCode !== 404) { +- await deleteIndex(indexName, logger, esClient); ++ await deleteIndex(indexName, logger, esClient) + } +- await createIndex(indexName, logger, esClient); ++ await createIndex(indexName, logger, esClient) + + // get data from db + logger.info({ + component: 'indexBulkDataToES', +- message: 'Getting data from database', +- }); +- const model = models[modelName]; +- const data = await model.findAll({ include }); +- const rawObjects = _.map(data, (r) => r.toJSON()); ++ message: 'Getting data from database' ++ }) ++ const model = models[modelName] ++ const data = await model.findAll({ include }) ++ const rawObjects = _.map(data, (r) => r.toJSON()) + if (_.isEmpty(rawObjects)) { + logger.info({ + component: 'indexBulkDataToES', +- message: `No data in database for ${modelName}`, +- }); +- return; ++ message: `No data in database for ${modelName}` ++ }) ++ return + } +- const bulks = getBulksFromDocuments(rawObjects); ++ const bulks = getBulksFromDocuments(rawObjects) + +- const startTime = Date.now(); +- let doneCount = 0; ++ const startTime = Date.now() ++ let doneCount = 0 + for (const bulk of bulks) { + // send bulk to esclient + const body = bulk.flatMap((doc) => [ + { index: { _index: indexName, _id: doc.id } }, +- doc, +- ]); +- await esClient.bulk({ refresh: true, body }); +- doneCount += bulk.length; ++ doc ++ ]) ++ await esClient.bulk({ refresh: true, body }) ++ doneCount += bulk.length + + // log metrics +- const timeSpent = Date.now() - startTime; +- const avgTimePerDocument = timeSpent / doneCount; +- const estimatedLength = avgTimePerDocument * data.length; +- const timeLeft = startTime + estimatedLength - Date.now(); ++ const timeSpent = Date.now() - startTime ++ const avgTimePerDocument = timeSpent / doneCount ++ const estimatedLength = avgTimePerDocument * data.length ++ const timeLeft = startTime + estimatedLength - Date.now() + logger.info({ + component: 'indexBulkDataToES', + message: `Processed ${doneCount} of ${ +@@ -398,8 +426,8 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { + avgTimePerDocument + )}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime( + timeLeft +- )}`, +- }); ++ )}` ++ }) + } + } + +@@ -410,36 +438,36 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { + * @param {string} id the job id + * @param {Object} logger the logger object + */ +-async function indexDataToEsById(id, modelOpts, indexName, logger) { +- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; +- const include = _.get(modelOpts, 'include', []); ++async function indexDataToEsById (id, modelOpts, indexName, logger) { ++ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName ++ const include = _.get(modelOpts, 'include', []) + + logger.info({ + component: 'indexDataToEsById', +- message: `Reindexing of ${modelName} with id ${id} started!`, +- }); +- const esClient = getESClient(); ++ message: `Reindexing of ${modelName} with id ${id} started!` ++ }) ++ const esClient = getESClient() + + logger.info({ + component: 'indexDataToEsById', +- message: 'Getting data from database', +- }); +- const model = models[modelName]; ++ message: 'Getting data from database' ++ }) ++ const model = models[modelName] + +- const data = await model.findById(id, include); ++ const data = await model.findById(id, include) + logger.info({ + component: 'indexDataToEsById', +- message: 'Indexing data into Elasticsearch', +- }); ++ message: 'Indexing data into Elasticsearch' ++ }) + await esClient.index({ + index: indexName, + id: id, +- body: data.dataValues, +- }); ++ body: data.dataValues ++ }) + logger.info({ + component: 'indexDataToEsById', +- message: 'Indexing complete!', +- }); ++ message: 'Indexing complete!' ++ }) + } + + /** +@@ -448,68 +476,68 @@ async function indexDataToEsById(id, modelOpts, indexName, logger) { + * @param {Array} dataModels the data models to import + * @param {Object} logger the logger object + */ +-async function importData(pathToFile, dataModels, logger) { ++async function importData (pathToFile, dataModels, logger) { + // check if file exists + if (!fs.existsSync(pathToFile)) { +- throw new Error(`File with path ${pathToFile} does not exist`); ++ throw new Error(`File with path ${pathToFile} does not exist`) + } + + // clear database +- logger.info({ component: 'importData', message: 'Clearing database...' }); +- await models.sequelize.sync({ force: true }); ++ logger.info({ component: 'importData', message: 'Clearing database...' }) ++ await models.sequelize.sync({ force: true }) + +- let transaction = null; +- let currentModelName = null; ++ let transaction = null ++ let currentModelName = null + try { + // Start a transaction +- transaction = await models.sequelize.transaction(); +- const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()); ++ transaction = await models.sequelize.transaction() ++ const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()) + + for (let index = 0; index < dataModels.length; index += 1) { +- const modelOpts = dataModels[index]; +- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; +- const include = _.get(modelOpts, 'include', []); ++ const modelOpts = dataModels[index] ++ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName ++ const include = _.get(modelOpts, 'include', []) + +- currentModelName = modelName; +- const model = models[modelName]; +- const modelRecords = jsonData[modelName]; ++ currentModelName = modelName ++ const model = models[modelName] ++ const modelRecords = jsonData[modelName] + + if (modelRecords && modelRecords.length > 0) { + logger.info({ + component: 'importData', +- message: `Importing data for model: ${modelName}`, +- }); ++ message: `Importing data for model: ${modelName}` ++ }) + +- await model.bulkCreate(modelRecords, { include, transaction }); ++ await model.bulkCreate(modelRecords, { include, transaction }) + logger.info({ + component: 'importData', +- message: `Records imported for model: ${modelName} = ${modelRecords.length}`, +- }); ++ message: `Records imported for model: ${modelName} = ${modelRecords.length}` ++ }) + } else { + logger.info({ + component: 'importData', +- message: `No records to import for model: ${modelName}`, +- }); ++ message: `No records to import for model: ${modelName}` ++ }) + } + } + // commit transaction only if all things went ok + logger.info({ + component: 'importData', +- message: 'committing transaction to database...', +- }); +- await transaction.commit(); ++ message: 'committing transaction to database...' ++ }) ++ await transaction.commit() + } catch (error) { + logger.error({ + component: 'importData', +- message: `Error while writing data of model: ${currentModelName}`, +- }); ++ message: `Error while writing data of model: ${currentModelName}` ++ }) + // rollback all insert operations + if (transaction) { + logger.info({ + component: 'importData', +- message: 'rollback database transaction...', +- }); +- transaction.rollback(); ++ message: 'rollback database transaction...' ++ }) ++ transaction.rollback() + } + if (error.name && error.errors && error.fields) { + // For sequelize validation errors, we throw only fields with data that helps in debugging error, +@@ -519,11 +547,11 @@ async function importData(pathToFile, dataModels, logger) { + modelName: currentModelName, + name: error.name, + errors: error.errors, +- fields: error.fields, ++ fields: error.fields + }) +- ); ++ ) + } else { +- throw error; ++ throw error + } + } + +@@ -533,10 +561,10 @@ async function importData(pathToFile, dataModels, logger) { + include: [ + { + model: models.Interview, +- as: 'interviews', +- }, +- ], +- }; ++ as: 'interviews' ++ } ++ ] ++ } + const resourceBookingModelOpts = { + modelName: 'ResourceBooking', + include: [ +@@ -546,23 +574,24 @@ async function importData(pathToFile, dataModels, logger) { + include: [ + { + model: models.WorkPeriodPayment, +- as: 'payments', +- }, +- ], +- }, +- ], +- }; +- await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger); ++ as: 'payments' ++ } ++ ] ++ } ++ ] ++ } ++ await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) + await indexBulkDataToES( + jobCandidateModelOpts, + config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), + logger +- ); ++ ) + await indexBulkDataToES( + resourceBookingModelOpts, + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + logger +- ); ++ ) ++ await indexBulkDataToES('Role', config.get('esConfig.ES_INDEX_ROLE'), logger) + } + + /** +@@ -571,74 +600,74 @@ async function importData(pathToFile, dataModels, logger) { + * @param {Array} dataModels the data models to export + * @param {Object} logger the logger object + */ +-async function exportData(pathToFile, dataModels, logger) { ++async function exportData (pathToFile, dataModels, logger) { + logger.info({ + component: 'exportData', +- message: `Start Saving data to file with path ${pathToFile}....`, +- }); ++ message: `Start Saving data to file with path ${pathToFile}....` ++ }) + +- const allModelsRecords = {}; ++ const allModelsRecords = {} + for (let index = 0; index < dataModels.length; index += 1) { +- const modelOpts = dataModels[index]; +- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; +- const include = _.get(modelOpts, 'include', []); +- const modelRecords = await models[modelName].findAll({ include }); +- const rawRecords = _.map(modelRecords, (r) => r.toJSON()); +- allModelsRecords[modelName] = rawRecords; ++ const modelOpts = dataModels[index] ++ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName ++ const include = _.get(modelOpts, 'include', []) ++ const modelRecords = await models[modelName].findAll({ include }) ++ const rawRecords = _.map(modelRecords, (r) => r.toJSON()) ++ allModelsRecords[modelName] = rawRecords + logger.info({ + component: 'exportData', +- message: `Records loaded for model: ${modelName} = ${rawRecords.length}`, +- }); ++ message: `Records loaded for model: ${modelName} = ${rawRecords.length}` ++ }) + } + +- fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)); ++ fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)) + logger.info({ + component: 'exportData', +- message: 'End Saving data to file....', +- }); ++ message: 'End Saving data to file....' ++ }) + } + + /** + * Format a time in milliseconds into a human readable format + * @param {Date} milliseconds the number of milliseconds + */ +-function formatTime(millisec) { +- const ms = Math.floor(millisec % 1000); +- const secs = Math.floor((millisec / 1000) % 60); +- const mins = Math.floor((millisec / (1000 * 60)) % 60); +- const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24); +- const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7); +- const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4); +- const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12); +- const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)); +- +- let formattedTime = '0 milliseconds'; ++function formatTime (millisec) { ++ const ms = Math.floor(millisec % 1000) ++ const secs = Math.floor((millisec / 1000) % 60) ++ const mins = Math.floor((millisec / (1000 * 60)) % 60) ++ const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24) ++ const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7) ++ const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4) ++ const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12) ++ const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)) ++ ++ let formattedTime = '0 milliseconds' + if (ms > 0) { +- formattedTime = `${ms} milliseconds`; ++ formattedTime = `${ms} milliseconds` + } + if (secs > 0) { +- formattedTime = `${secs} seconds ${formattedTime}`; ++ formattedTime = `${secs} seconds ${formattedTime}` + } + if (mins > 0) { +- formattedTime = `${mins} minutes ${formattedTime}`; ++ formattedTime = `${mins} minutes ${formattedTime}` + } + if (hrs > 0) { +- formattedTime = `${hrs} hours ${formattedTime}`; ++ formattedTime = `${hrs} hours ${formattedTime}` + } + if (days > 0) { +- formattedTime = `${days} days ${formattedTime}`; ++ formattedTime = `${days} days ${formattedTime}` + } + if (weeks > 0) { +- formattedTime = `${weeks} weeks ${formattedTime}`; ++ formattedTime = `${weeks} weeks ${formattedTime}` + } + if (mnths > 0) { +- formattedTime = `${mnths} months ${formattedTime}`; ++ formattedTime = `${mnths} months ${formattedTime}` + } + if (yrs > 0) { +- formattedTime = `${yrs} years ${formattedTime}`; ++ formattedTime = `${yrs} years ${formattedTime}` + } + +- return formattedTime.trim(); ++ return formattedTime.trim() + } + + /** +@@ -647,30 +676,30 @@ function formatTime(millisec) { + * @param {Array} source the array in which to search for the term + * @param {Array | String} term the term to search + */ +-function checkIfExists(source, term) { +- let terms; ++function checkIfExists (source, term) { ++ let terms + + if (!_.isArray(source)) { +- throw new Error('Source argument should be an array'); ++ throw new Error('Source argument should be an array') + } + +- source = source.map((s) => s.toLowerCase()); ++ source = source.map((s) => s.toLowerCase()) + + if (_.isString(term)) { +- terms = term.toLowerCase().split(' '); ++ terms = term.toLowerCase().split(' ') + } else if (_.isArray(term)) { +- terms = term.map((t) => t.toLowerCase()); ++ terms = term.map((t) => t.toLowerCase()) + } else { +- throw new Error('Term argument should be either a string or an array'); ++ throw new Error('Term argument should be either a string or an array') + } + + for (let i = 0; i < terms.length; i++) { + if (source.includes(terms[i])) { +- return true; ++ return true + } + } + +- return false; ++ return false + } + + /** +@@ -678,10 +707,10 @@ function checkIfExists(source, term) { + * @param {Function} fn the async function + * @returns {Function} the wrapped function + */ +-function wrapExpress(fn) { ++function wrapExpress (fn) { + return function (req, res, next) { +- fn(req, res, next).catch(next); +- }; ++ fn(req, res, next).catch(next) ++ } + } + + /** +@@ -689,20 +718,20 @@ function wrapExpress(fn) { + * @param obj the object (controller exports) + * @returns {Object|Array} the wrapped object + */ +-function autoWrapExpress(obj) { ++function autoWrapExpress (obj) { + if (_.isArray(obj)) { +- return obj.map(autoWrapExpress); ++ return obj.map(autoWrapExpress) + } + if (_.isFunction(obj)) { + if (obj.constructor.name === 'AsyncFunction') { +- return wrapExpress(obj); ++ return wrapExpress(obj) + } +- return obj; ++ return obj + } + _.each(obj, (value, key) => { +- obj[key] = autoWrapExpress(value); +- }); +- return obj; ++ obj[key] = autoWrapExpress(value) ++ }) ++ return obj + } + + /** +@@ -711,11 +740,11 @@ function autoWrapExpress(obj) { + * @param {Number} page the page number + * @returns {String} link for the page + */ +-function getPageLink(req, page) { +- const q = _.assignIn({}, req.query, { page }); ++function getPageLink (req, page) { ++ const q = _.assignIn({}, req.query, { page }) + return `${req.protocol}://${req.get('Host')}${req.baseUrl}${ + req.path +- }?${querystring.stringify(q)}`; ++ }?${querystring.stringify(q)}` + } + + /** +@@ -724,31 +753,31 @@ function getPageLink(req, page) { + * @param {Object} res the HTTP response + * @param {Object} result the operation result + */ +-function setResHeaders(req, res, result) { +- const totalPages = Math.ceil(result.total / result.perPage); ++function setResHeaders (req, res, result) { ++ const totalPages = Math.ceil(result.total / result.perPage) + if (result.page > 1) { +- res.set('X-Prev-Page', result.page - 1); ++ res.set('X-Prev-Page', result.page - 1) + } + if (result.page < totalPages) { +- res.set('X-Next-Page', result.page + 1); ++ res.set('X-Next-Page', result.page + 1) + } +- res.set('X-Page', result.page); +- res.set('X-Per-Page', result.perPage); +- res.set('X-Total', result.total); +- res.set('X-Total-Pages', totalPages); ++ res.set('X-Page', result.page) ++ res.set('X-Per-Page', result.perPage) ++ res.set('X-Total', result.total) ++ res.set('X-Total-Pages', totalPages) + // set Link header + if (totalPages > 0) { + let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink( + req, + totalPages +- )}>; rel="last"`; ++ )}>; rel="last"` + if (result.page > 1) { +- link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"`; ++ link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"` + } + if (result.page < totalPages) { +- link += `, <${getPageLink(req, result.page + 1)}>; rel="next"`; ++ link += `, <${getPageLink(req, result.page + 1)}>; rel="next"` + } +- res.set('Link', link); ++ res.set('Link', link) + } + } + +@@ -756,30 +785,30 @@ function setResHeaders(req, res, result) { + * Get ES Client + * @return {Object} Elastic Host Client Instance + */ +-function getESClient() { ++function getESClient () { + if (esClients.client) { +- return esClients.client; ++ return esClients.client + } + +- const host = config.esConfig.HOST; +- const cloudId = config.esConfig.ELASTICCLOUD.id; ++ const host = config.esConfig.HOST ++ const cloudId = config.esConfig.ELASTICCLOUD.id + if (cloudId) { + // Elastic Cloud configuration + esClients.client = new elasticsearch.Client({ + cloud: { +- id: cloudId, ++ id: cloudId + }, + auth: { + username: config.esConfig.ELASTICCLOUD.username, +- password: config.esConfig.ELASTICCLOUD.password, +- }, +- }); ++ password: config.esConfig.ELASTICCLOUD.password ++ } ++ }) + } else { + esClients.client = new elasticsearch.Client({ +- node: host, +- }); ++ node: host ++ }) + } +- return esClients.client; ++ return esClients.client + } + + /* +@@ -790,8 +819,8 @@ const getM2MToken = async () => { + return await m2m.getMachineToken( + config.AUTH0_CLIENT_ID, + config.AUTH0_CLIENT_SECRET +- ); +-}; ++ ) ++} + + /* + * Function to get M2M token for U-Bahn +@@ -801,8 +830,8 @@ const getM2MUbahnToken = async () => { + return await m2mForUbahn.getMachineToken( + config.AUTH0_CLIENT_ID, + config.AUTH0_CLIENT_SECRET +- ); +-}; ++ ) ++} + + /** + * Function to encode query string +@@ -810,17 +839,17 @@ const getM2MUbahnToken = async () => { + * @param {String} nesting the nesting string + * @returns {String} query string + */ +-function encodeQueryString(queryObj, nesting = '') { ++function encodeQueryString (queryObj, nesting = '') { + const pairs = Object.entries(queryObj).map(([key, val]) => { + // Handle the nested, recursive case, where the value to encode is an object itself + if (typeof val === 'object') { +- return encodeQueryString(val, nesting + `${key}.`); ++ return encodeQueryString(val, nesting + `${key}.`) + } else { + // Handle base case, where the value to encode is simply a string. +- return [nesting + key, val].map(querystring.escape).join('='); ++ return [nesting + key, val].map(querystring.escape).join('=') + } +- }); +- return pairs.join('&'); ++ }) ++ return pairs.join('&') + } + + /** +@@ -828,31 +857,31 @@ function encodeQueryString(queryObj, nesting = '') { + * @param {Integer} externalId the legacy user id + * @returns {Array} the users found + */ +-async function listUsersByExternalId(externalId) { ++async function listUsersByExternalId (externalId) { + // return empty list if externalId is null or undefined + if (!!externalId !== true) { +- return []; ++ return [] + } + +- const token = await getM2MUbahnToken(); ++ const token = await getM2MUbahnToken() + const q = { + enrich: true, + externalProfile: { + organizationId: config.ORG_ID, +- externalId, +- }, +- }; +- const url = `${config.TC_API}/users?${encodeQueryString(q)}`; ++ externalId ++ } ++ } ++ const url = `${config.TC_API}/users?${encodeQueryString(q)}` + const res = await request + .get(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'listUserByExternalId', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return res.body; ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return res.body + } + + /** +@@ -860,14 +889,14 @@ async function listUsersByExternalId(externalId) { + * @param {Integer} externalId the legacy user id + * @returns {Object} the user + */ +-async function getUserByExternalId(externalId) { +- const users = await listUsersByExternalId(externalId); ++async function getUserByExternalId (externalId) { ++ const users = await listUsersByExternalId(externalId) + if (_.isEmpty(users)) { + throw new errors.NotFoundError( + `externalId: ${externalId} "user" not found` +- ); ++ ) + } +- return users[0]; ++ return users[0] + } + + /** +@@ -876,24 +905,24 @@ async function getUserByExternalId(externalId) { + * @params {Object} payload the payload + * @params {Object} options the extra options to control the function + */ +-async function postEvent(topic, payload, options = {}) { ++async function postEvent (topic, payload, options = {}) { + logger.debug({ + component: 'helper', + context: 'postEvent', + message: `Posting event to Kafka topic ${topic}, ${JSON.stringify( + payload +- )}`, +- }); +- const client = getBusApiClient(); ++ )}` ++ }) ++ const client = getBusApiClient() + const message = { + topic, + originator: config.KAFKA_MESSAGE_ORIGINATOR, + timestamp: new Date().toISOString(), + 'mime-type': 'application/json', +- payload, +- }; +- await client.postEvent(message); +- await eventDispatcher.handleEvent(topic, { value: payload, options }); ++ payload ++ } ++ await client.postEvent(message) ++ await eventDispatcher.handleEvent(topic, { value: payload, options }) + } + + /** +@@ -902,11 +931,11 @@ async function postEvent(topic, payload, options = {}) { + * @param {Object} err the err + * @returns {Boolean} the result + */ +-function isDocumentMissingException(err) { ++function isDocumentMissingException (err) { + if (err.statusCode === 404 && err instanceof ESResponseError) { +- return true; ++ return true + } +- return false; ++ return false + } + + /** +@@ -915,34 +944,34 @@ function isDocumentMissingException(err) { + * @param {Object} criteria the search criteria + * @returns the request result + */ +-async function getProjects(currentUser, criteria = {}) { +- let token; ++async function getProjects (currentUser, criteria = {}) { ++ let token + if (currentUser.hasManagePermission || currentUser.isMachine) { +- const m2mToken = await getM2MToken(); +- token = `Bearer ${m2mToken}`; ++ const m2mToken = await getM2MToken() ++ token = `Bearer ${m2mToken}` + } else { +- token = currentUser.jwtToken; ++ token = currentUser.jwtToken + } +- const url = `${config.TC_API}/projects?type=talent-as-a-service`; ++ const url = `${config.TC_API}/projects?type=talent-as-a-service` + const res = await request + .get(url) + .query(criteria) + .set('Authorization', token) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getProjects', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) + const result = _.map(res.body, (item) => { +- return _.pick(item, ['id', 'name', 'invites', 'members']); +- }); ++ return _.pick(item, ['id', 'name', 'invites', 'members']) ++ }) + return { + total: Number(_.get(res.headers, 'x-total')), + page: Number(_.get(res.headers, 'x-page')), + perPage: Number(_.get(res.headers, 'x-per-page')), +- result, +- }; ++ result ++ } + } + + /** +@@ -951,24 +980,24 @@ async function getProjects(currentUser, criteria = {}) { + * @param {String} userId the legacy user id + * @returns {Object} the user + */ +-async function getTopcoderUserById(userId) { +- const token = await getM2MToken(); ++async function getTopcoderUserById (userId) { ++ const token = await getM2MToken() + const res = await request + .get(config.TOPCODER_USERS_API) + .query({ filter: `id=${userId}` }) + .set('Authorization', `Bearer ${token}`) +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getTopcoderUserById', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- const user = _.get(res.body, 'result.content[0]'); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ const user = _.get(res.body, 'result.content[0]') + if (!user) { + throw new errors.NotFoundError( + `userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}` +- ); ++ ) + } +- return user; ++ return user + } + + /** +@@ -976,31 +1005,31 @@ async function getTopcoderUserById(userId) { + * @param {String} userId the user id + * @returns the request result + */ +-async function getUserById(userId, enrich) { +- const token = await getM2MUbahnToken(); ++async function getUserById (userId, enrich) { ++ const token = await getM2MUbahnToken() + const res = await request + .get(`${config.TC_API}/users/${userId}` + (enrich ? '?enrich=true' : '')) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getUserById', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) + +- const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']); ++ const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']) + + if (enrich) { + user.skills = (res.body.skills || []).map((skillObj) => + _.pick(skillObj.skill, ['id', 'name']) +- ); +- const attributes = _.get(res, 'body.attributes', []); ++ ) ++ const attributes = _.get(res, 'body.attributes', []) + user.attributes = _.map(attributes, (attr) => + _.pick(attr, ['id', 'value', 'attribute.id', 'attribute.name']) +- ); ++ ) + } + +- return user; ++ return user + } + + /** +@@ -1008,19 +1037,19 @@ async function getUserById(userId, enrich) { + * @param {Object} data the user data + * @returns the request result + */ +-async function createUbahnUser({ handle, firstName, lastName }) { +- const token = await getM2MUbahnToken(); ++async function createUbahnUser ({ handle, firstName, lastName }) { ++ const token = await getM2MUbahnToken() + const res = await request + .post(`${config.TC_API}/users`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') +- .send({ handle, firstName, lastName }); ++ .send({ handle, firstName, lastName }) + localLogger.debug({ + context: 'createUbahnUser', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return _.pick(res.body, ['id']); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return _.pick(res.body, ['id']) + } + + /** +@@ -1028,21 +1057,21 @@ async function createUbahnUser({ handle, firstName, lastName }) { + * @param {String} userId the user id(with uuid format) + * @param {Object} data the profile data + */ +-async function createUserExternalProfile( ++async function createUserExternalProfile ( + userId, + { organizationId, externalId } + ) { +- const token = await getM2MUbahnToken(); ++ const token = await getM2MUbahnToken() + const res = await request + .post(`${config.TC_API}/users/${userId}/externalProfiles`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') +- .send({ organizationId, externalId: String(externalId) }); ++ .send({ organizationId, externalId: String(externalId) }) + localLogger.debug({ + context: 'createUserExternalProfile', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) + } + + /** +@@ -1050,23 +1079,23 @@ async function createUserExternalProfile( + * @param {Array} handles the handle array + * @returns the request result + */ +-async function getMembers(handles) { +- const token = await getM2MToken(); ++async function getMembers (handles) { ++ const token = await getM2MToken() + const handlesStr = _.map(handles, (handle) => { +- return '%22' + handle.toLowerCase() + '%22'; +- }).join(','); +- const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]`; ++ return '%22' + handle.toLowerCase() + '%22' ++ }).join(',') ++ const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]` + + const res = await request + .get(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getMembers', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return res.body; ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return res.body + } + + /** +@@ -1075,36 +1104,36 @@ async function getMembers(handles) { + * @param {Number} id project id + * @returns the request result + */ +-async function getProjectById(currentUser, id) { +- let token; ++async function getProjectById (currentUser, id) { ++ let token + if (currentUser.hasManagePermission || currentUser.isMachine) { +- const m2mToken = await getM2MToken(); +- token = `Bearer ${m2mToken}`; ++ const m2mToken = await getM2MToken() ++ token = `Bearer ${m2mToken}` + } else { +- token = currentUser.jwtToken; ++ token = currentUser.jwtToken + } +- const url = `${config.TC_API}/projects/${id}`; ++ const url = `${config.TC_API}/projects/${id}` + try { + const res = await request + .get(url) + .set('Authorization', token) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getProjectById', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return _.pick(res.body, ['id', 'name', 'invites', 'members']); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return _.pick(res.body, ['id', 'name', 'invites', 'members']) + } catch (err) { + if (err.status === HttpStatus.FORBIDDEN) { + throw new errors.ForbiddenError( + `You are not allowed to access the project with id ${id}` +- ); ++ ) + } + if (err.status === HttpStatus.NOT_FOUND) { +- throw new errors.NotFoundError(`id: ${id} project not found`); ++ throw new errors.NotFoundError(`id: ${id} project not found`) + } +- throw err; ++ throw err + } + } + +@@ -1115,33 +1144,33 @@ async function getProjectById(currentUser, id) { + * @param {Object} criteria the search criteria + * @returns the request result + */ +-async function getTopcoderSkills(criteria) { +- const token = await getM2MUbahnToken(); ++async function getTopcoderSkills (criteria) { ++ const token = await getM2MUbahnToken() + try { + const res = await request + .get(`${config.TC_API}/skills`) + .query({ + skillProviderId: config.TOPCODER_SKILL_PROVIDER_ID, +- ...criteria, ++ ...criteria + }) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getTopcoderSkills', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) + return { + total: Number(_.get(res.headers, 'x-total')), + page: Number(_.get(res.headers, 'x-page')), + perPage: Number(_.get(res.headers, 'x-per-page')), +- result: res.body, +- }; ++ result: res.body ++ } + } catch (err) { + if (err.status === HttpStatus.BAD_REQUEST) { +- throw new errors.BadRequestError(err.response.body.message); ++ throw new errors.BadRequestError(err.response.body.message) + } +- throw err; ++ throw err + } + } + +@@ -1150,18 +1179,18 @@ async function getTopcoderSkills(criteria) { + * @param {String} skillId the skill Id + * @returns the request result + */ +-async function getSkillById(skillId) { +- const token = await getM2MUbahnToken(); ++async function getSkillById (skillId) { ++ const token = await getM2MUbahnToken() + const res = await request + .get(`${config.TC_API}/skills/${skillId}`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getSkillById', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return _.pick(res.body, ['id', 'name']); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return _.pick(res.body, ['id', 'name']) + } + + /** +@@ -1174,22 +1203,22 @@ async function getSkillById(skillId) { + * @params {Object} currentUser the user who perform this operation + * @returns {String} the ubahn user id + */ +-async function ensureUbahnUserId(currentUser) { ++async function ensureUbahnUserId (currentUser) { + try { +- return (await getUserByExternalId(currentUser.userId)).id; ++ return (await getUserByExternalId(currentUser.userId)).id + } catch (err) { + if (!(err instanceof errors.NotFoundError)) { +- throw err; ++ throw err + } +- const topcoderUser = await getTopcoderUserById(currentUser.userId); ++ const topcoderUser = await getTopcoderUserById(currentUser.userId) + const user = await createUbahnUser( + _.pick(topcoderUser, ['handle', 'firstName', 'lastName']) +- ); ++ ) + await createUserExternalProfile(user.id, { + organizationId: config.ORG_ID, +- externalId: currentUser.userId, +- }); +- return user.id; ++ externalId: currentUser.userId ++ }) ++ return user.id + } + } + +@@ -1199,8 +1228,8 @@ async function ensureUbahnUserId(currentUser) { + * @param {String} jobId the job id + * @returns {Object} the job data + */ +-async function ensureJobById(jobId) { +- return models.Job.findById(jobId); ++async function ensureJobById (jobId) { ++ return models.Job.findById(jobId) + } + + /** +@@ -1209,8 +1238,8 @@ async function ensureJobById(jobId) { + * @param {String} resourceBookingId the resourceBooking id + * @returns {Object} the resourceBooking data + */ +-async function ensureResourceBookingById(resourceBookingId) { +- return models.ResourceBooking.findById(resourceBookingId); ++async function ensureResourceBookingById (resourceBookingId) { ++ return models.ResourceBooking.findById(resourceBookingId) + } + + /** +@@ -1218,8 +1247,8 @@ async function ensureResourceBookingById(resourceBookingId) { + * @param {String} workPeriodId the workPeriod id + * @returns the workPeriod data + */ +-async function ensureWorkPeriodById(workPeriodId) { +- return models.WorkPeriod.findById(workPeriodId); ++async function ensureWorkPeriodById (workPeriodId) { ++ return models.WorkPeriod.findById(workPeriodId) + } + + /** +@@ -1228,24 +1257,24 @@ async function ensureWorkPeriodById(workPeriodId) { + * @param {String} jobId the user id + * @returns {Object} the user data + */ +-async function ensureUserById(userId) { +- const token = await getM2MUbahnToken(); ++async function ensureUserById (userId) { ++ const token = await getM2MUbahnToken() + try { + const res = await request + .get(`${config.TC_API}/users/${userId}`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'ensureUserById', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return res.body; ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return res.body + } catch (err) { + if (err.status === HttpStatus.NOT_FOUND) { +- throw new errors.NotFoundError(`id: ${userId} "user" not found`); ++ throw new errors.NotFoundError(`id: ${userId} "user" not found`) + } +- throw err; ++ throw err + } + } + +@@ -1254,12 +1283,12 @@ async function ensureUserById(userId) { + * + * @returns {Object} the M2M auth user + */ +-function getAuditM2Muser() { ++function getAuditM2Muser () { + return { + isMachine: true, + userId: config.m2m.M2M_AUDIT_USER_ID, +- handle: config.m2m.M2M_AUDIT_HANDLE, +- }; ++ handle: config.m2m.M2M_AUDIT_HANDLE ++ } + } + + /** +@@ -1271,24 +1300,24 @@ function getAuditM2Muser() { + * @param {Number} projectId project id + * @returns the result + */ +-async function checkIsMemberOfProject(userId, projectId) { +- const m2mToken = await getM2MToken(); ++async function checkIsMemberOfProject (userId, projectId) { ++ const m2mToken = await getM2MToken() + const res = await request + .get(`${config.TC_API}/projects/${projectId}`) + .set('Authorization', `Bearer ${m2mToken}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); +- const memberIdList = _.map(res.body.members, 'userId'); ++ .set('Accept', 'application/json') ++ const memberIdList = _.map(res.body.members, 'userId') + localLogger.debug({ + context: 'checkIsMemberOfProject', + message: `the members of project ${projectId}: ${JSON.stringify( + memberIdList +- )}, authUserId: ${JSON.stringify(userId)}`, +- }); ++ )}, authUserId: ${JSON.stringify(userId)}` ++ }) + if (!memberIdList.includes(userId)) { + throw new errors.UnauthorizedError( + `userId: ${userId} the user is not a member of project ${projectId}` +- ); ++ ) + } + } + +@@ -1298,11 +1327,11 @@ async function checkIsMemberOfProject(userId, projectId) { + * @param {Array} handles the array of handles + * @returns {Array} the member details + */ +-async function getMemberDetailsByHandles(handles) { ++async function getMemberDetailsByHandles (handles) { + if (!handles.length) { +- return []; ++ return [] + } +- const token = await getM2MToken(); ++ const token = await getM2MToken() + const res = await request + .get(`${config.TOPCODER_MEMBERS_API}/_search`) + .query({ +@@ -1310,15 +1339,15 @@ async function getMemberDetailsByHandles(handles) { + handles, + (handle) => `handleLower:${handle.toLowerCase()}` + ).join(' OR '), +- fields: 'userId,handle,firstName,lastName,email', ++ fields: 'userId,handle,firstName,lastName,email' + }) + .set('Authorization', `Bearer ${token}`) +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getMemberDetailsByHandles', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return _.get(res.body, 'result.content'); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return _.get(res.body, 'result.content') + } + + /** +@@ -1327,17 +1356,17 @@ async function getMemberDetailsByHandles(handles) { + * @param {String} handle the user handle + * @returns {Object} the member details + */ +-async function getV3MemberDetailsByHandle(handle) { +- const token = await getM2MToken(); ++async function getV3MemberDetailsByHandle (handle) { ++ const token = await getM2MToken() + const res = await request + .get(`${config.TOPCODER_MEMBERS_API}/${handle}`) + .set('Authorization', `Bearer ${token}`) +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getV3MemberDetailsByHandle', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return _.get(res.body, 'result.content'); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return _.get(res.body, 'result.content') + } + + /** +@@ -1347,20 +1376,20 @@ async function getV3MemberDetailsByHandle(handle) { + * @param {String} email the email + * @returns {Array} the member details + */ +-async function _getMemberDetailsByEmail(token, email) { ++async function _getMemberDetailsByEmail (token, email) { + const res = await request + .get(config.TOPCODER_USERS_API) + .query({ + filter: `email=${email}`, +- fields: 'handle,id,email,firstName,lastName', ++ fields: 'handle,id,email,firstName,lastName' + }) + .set('Authorization', `Bearer ${token}`) +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: '_getMemberDetailsByEmail', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return _.get(res.body, 'result.content'); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return _.get(res.body, 'result.content') + } + + /** +@@ -1370,25 +1399,25 @@ async function _getMemberDetailsByEmail(token, email) { + * @param {Array} emails the array of emails + * @returns {Array} the member details + */ +-async function getMemberDetailsByEmails(emails) { +- const token = await getM2MToken(); ++async function getMemberDetailsByEmails (emails) { ++ const token = await getM2MToken() + const limiter = new Bottleneck({ +- maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API, +- }); ++ maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API ++ }) + const membersArray = await Promise.all( + emails.map((email) => + limiter.schedule(() => + _getMemberDetailsByEmail(token, email).catch((error) => { + localLogger.error({ + context: 'getMemberDetailsByEmails', +- message: error.message, +- }); +- return []; ++ message: error.message ++ }) ++ return [] + }) + ) + ) +- ); +- return _.flatten(membersArray); ++ ) ++ return _.flatten(membersArray) + } + + /** +@@ -1399,20 +1428,20 @@ async function getMemberDetailsByEmails(emails) { + * @param {Object} criteria the filtering criteria + * @returns {Object} the member created + */ +-async function createProjectMember(projectId, data, criteria) { +- const m2mToken = await getM2MToken(); ++async function createProjectMember (projectId, data, criteria) { ++ const m2mToken = await getM2MToken() + const { body: member } = await request + .post(`${config.TC_API}/projects/${projectId}/members`) + .set('Authorization', `Bearer ${m2mToken}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .query(criteria) +- .send(data); ++ .send(data) + localLogger.debug({ + context: 'createProjectMember', +- message: `response body: ${JSON.stringify(member)}`, +- }); +- return member; ++ message: `response body: ${JSON.stringify(member)}` ++ }) ++ return member + } + + /** +@@ -1422,21 +1451,21 @@ async function createProjectMember(projectId, data, criteria) { + * @param {Object} criteria the search criteria + * @returns {Array} the project members + */ +-async function listProjectMembers(currentUser, projectId, criteria = {}) { ++async function listProjectMembers (currentUser, projectId, criteria = {}) { + const token = + currentUser.hasManagePermission || currentUser.isMachine + ? `Bearer ${await getM2MToken()}` +- : currentUser.jwtToken; ++ : currentUser.jwtToken + const { body: members } = await request + .get(`${config.TC_API}/projects/${projectId}/members`) + .query(criteria) + .set('Authorization', token) +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'listProjectMembers', +- message: `response body: ${JSON.stringify(members)}`, +- }); +- return members; ++ message: `response body: ${JSON.stringify(members)}` ++ }) ++ return members + } + + /** +@@ -1446,21 +1475,21 @@ async function listProjectMembers(currentUser, projectId, criteria = {}) { + * @param {Object} criteria the search criteria + * @returns {Array} the member invites + */ +-async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { ++async function listProjectMemberInvites (currentUser, projectId, criteria = {}) { + const token = + currentUser.hasManagePermission || currentUser.isMachine + ? `Bearer ${await getM2MToken()}` +- : currentUser.jwtToken; ++ : currentUser.jwtToken + const { body: invites } = await request + .get(`${config.TC_API}/projects/${projectId}/invites`) + .query(criteria) + .set('Authorization', token) +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'listProjectMemberInvites', +- message: `response body: ${JSON.stringify(invites)}`, +- }); +- return invites; ++ message: `response body: ${JSON.stringify(invites)}` ++ }) ++ return invites + } + + /** +@@ -1470,24 +1499,24 @@ async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { + * @param {String} projectMemberId the id of the project member + * @returns {undefined} + */ +-async function deleteProjectMember(currentUser, projectId, projectMemberId) { ++async function deleteProjectMember (currentUser, projectId, projectMemberId) { + const token = + currentUser.hasManagePermission || currentUser.isMachine + ? `Bearer ${await getM2MToken()}` +- : currentUser.jwtToken; ++ : currentUser.jwtToken + try { + await request + .delete( + `${config.TC_API}/projects/${projectId}/members/${projectMemberId}` + ) +- .set('Authorization', token); ++ .set('Authorization', token) + } catch (err) { + if (err.status === HttpStatus.NOT_FOUND) { + throw new errors.NotFoundError( + `projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}` +- ); ++ ) + } +- throw err; ++ throw err + } + } + +@@ -1497,13 +1526,13 @@ async function deleteProjectMember(currentUser, projectId, projectMemberId) { + * @param {String} attributeName Requested attribute name, e.g. "email" + * @returns attribute value + */ +-function getUserAttributeValue(user, attributeName) { +- const attributes = _.get(user, 'attributes', []); ++function getUserAttributeValue (user, attributeName) { ++ const attributes = _.get(user, 'attributes', []) + const targetAttribute = _.find( + attributes, + (a) => a.attribute.name === attributeName +- ); +- return _.get(targetAttribute, 'value'); ++ ) ++ return _.get(targetAttribute, 'value') + } + + /** +@@ -1513,34 +1542,34 @@ function getUserAttributeValue(user, attributeName) { + * @param {String} token m2m token + * @returns {Object} the challenge created + */ +-async function createChallenge(data, token) { ++async function createChallenge (data, token) { + if (!token) { +- token = await getM2MToken(); ++ token = await getM2MToken() + } +- const url = `${config.TC_API}/challenges`; ++ const url = `${config.TC_API}/challenges` + localLogger.debug({ + context: 'createChallenge', +- message: `EndPoint: POST ${url}`, +- }); ++ message: `EndPoint: POST ${url}` ++ }) + localLogger.debug({ + context: 'createChallenge', +- message: `Request Body: ${JSON.stringify(data)}`, +- }); ++ message: `Request Body: ${JSON.stringify(data)}` ++ }) + const { body: challenge, status: httpStatus } = await request + .post(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') +- .send(data); ++ .send(data) + localLogger.debug({ + context: 'createChallenge', +- message: `Status Code: ${httpStatus}`, +- }); ++ message: `Status Code: ${httpStatus}` ++ }) + localLogger.debug({ + context: 'createChallenge', +- message: `Response Body: ${JSON.stringify(challenge)}`, +- }); +- return challenge; ++ message: `Response Body: ${JSON.stringify(challenge)}` ++ }) ++ return challenge + } + + /** +@@ -1551,34 +1580,34 @@ async function createChallenge(data, token) { + * @param {String} token m2m token + * @returns {Object} the challenge updated + */ +-async function updateChallenge(challengeId, data, token) { ++async function updateChallenge (challengeId, data, token) { + if (!token) { +- token = await getM2MToken(); ++ token = await getM2MToken() + } +- const url = `${config.TC_API}/challenges/${challengeId}`; ++ const url = `${config.TC_API}/challenges/${challengeId}` + localLogger.debug({ + context: 'updateChallenge', +- message: `EndPoint: PATCH ${url}`, +- }); ++ message: `EndPoint: PATCH ${url}` ++ }) + localLogger.debug({ + context: 'updateChallenge', +- message: `Request Body: ${JSON.stringify(data)}`, +- }); ++ message: `Request Body: ${JSON.stringify(data)}` ++ }) + const { body: challenge, status: httpStatus } = await request + .patch(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') +- .send(data); ++ .send(data) + localLogger.debug({ + context: 'updateChallenge', +- message: `Status Code: ${httpStatus}`, +- }); ++ message: `Status Code: ${httpStatus}` ++ }) + localLogger.debug({ + context: 'updateChallenge', +- message: `Response Body: ${JSON.stringify(challenge)}`, +- }); +- return challenge; ++ message: `Response Body: ${JSON.stringify(challenge)}` ++ }) ++ return challenge + } + + /** +@@ -1588,34 +1617,34 @@ async function updateChallenge(challengeId, data, token) { + * @param {String} token m2m token + * @returns {Object} the resource created + */ +-async function createChallengeResource(data, token) { ++async function createChallengeResource (data, token) { + if (!token) { +- token = await getM2MToken(); ++ token = await getM2MToken() + } +- const url = `${config.TC_API}/resources`; ++ const url = `${config.TC_API}/resources` + localLogger.debug({ + context: 'createChallengeResource', +- message: `EndPoint: POST ${url}`, +- }); ++ message: `EndPoint: POST ${url}` ++ }) + localLogger.debug({ + context: 'createChallengeResource', +- message: `Request Body: ${JSON.stringify(data)}`, +- }); ++ message: `Request Body: ${JSON.stringify(data)}` ++ }) + const { body: resource, status: httpStatus } = await request + .post(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') +- .send(data); ++ .send(data) + localLogger.debug({ + context: 'createChallengeResource', +- message: `Status Code: ${httpStatus}`, +- }); ++ message: `Status Code: ${httpStatus}` ++ }) + localLogger.debug({ + context: 'createChallengeResource', +- message: `Response Body: ${JSON.stringify(resource)}`, +- }); +- return resource; ++ message: `Response Body: ${JSON.stringify(resource)}` ++ }) ++ return resource + } + + /** +@@ -1624,40 +1653,40 @@ async function createChallengeResource(data, token) { + * @param {Date} end end date of the resource booking + * @returns {Array<{startDate:Date, endDate:Date, daysWorked:number}>} information about workPeriods + */ +-function extractWorkPeriods(start, end) { ++function extractWorkPeriods (start, end) { + // calculate maximum possible daysWorked for a week +- function getDaysWorked(week) { ++ function getDaysWorked (week) { + if (weeks === 1) { +- return Math.min(endDay, 5) - Math.max(startDay, 1) + 1; ++ return Math.min(endDay, 5) - Math.max(startDay, 1) + 1 + } else if (week === 0) { +- return Math.min(6 - startDay, 5); ++ return Math.min(6 - startDay, 5) + } else if (week === weeks - 1) { +- return Math.min(endDay, 5); +- } else return 5; ++ return Math.min(endDay, 5) ++ } else return 5 + } +- const periods = []; ++ const periods = [] + if (_.isNil(start) || _.isNil(end)) { +- return periods; ++ return periods + } +- const startDate = moment(start); +- const startDay = startDate.get('day'); +- startDate.set('day', 0).startOf('day'); ++ const startDate = moment(start) ++ const startDay = startDate.get('day') ++ startDate.set('day', 0).startOf('day') + +- const endDate = moment(end); +- const endDay = endDate.get('day'); +- endDate.set('day', 6).endOf('day'); ++ const endDate = moment(end) ++ const endDay = endDate.get('day') ++ endDate.set('day', 6).endOf('day') + +- const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7; ++ const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7 + + for (let i = 0; i < weeks; i++) { + periods.push({ + startDate: startDate.format('YYYY-MM-DD'), + endDate: startDate.add(6, 'day').format('YYYY-MM-DD'), +- daysWorked: getDaysWorked(i), +- }); +- startDate.add(1, 'day'); ++ daysWorked: getDaysWorked(i) ++ }) ++ startDate.add(1, 'day') + } +- return periods; ++ return periods + } + + /** +@@ -1666,19 +1695,19 @@ function extractWorkPeriods(start, end) { + * @param {String} userHandle user handle + * @returns {String} email address of the user + */ +-async function getUserByHandle(userHandle) { +- const token = await getM2MToken(); +- const url = `${config.TC_API}/members/${userHandle}`; ++async function getUserByHandle (userHandle) { ++ const token = await getM2MToken() ++ const url = `${config.TC_API}/members/${userHandle}` + const res = await request + .get(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getUserByHandle', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return _.get(res, 'body'); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return _.get(res, 'body') + } + + /** +@@ -1687,14 +1716,14 @@ async function getUserByHandle(userHandle) { + * @param {*} object of json that would be replaced in string + * @returns + */ +-async function substituteStringByObject(string, object) { ++async function substituteStringByObject (string, object) { + for (var key in object) { + if (!Object.prototype.hasOwnProperty.call(object, key)) { +- continue; ++ continue + } +- string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]); ++ string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]) + } +- return string; ++ return string + } + + /** +@@ -1702,19 +1731,19 @@ async function substituteStringByObject(string, object) { + * @param {Object} data title of project and any other info + * @returns {Object} the project created + */ +-async function createProject(currentUser, data) { +- const token = currentUser.jwtToken; ++async function createProject (currentUser, data) { ++ const token = currentUser.jwtToken + const res = await request + .post(`${config.TC_API}/projects/`) + .set('Authorization', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') +- .send(data); ++ .send(data) + localLogger.debug({ + context: 'createProject', +- message: `response body: ${JSON.stringify(res)}`, +- }); +- return _.get(res, 'body'); ++ message: `response body: ${JSON.stringify(res)}` ++ }) ++ return _.get(res, 'body') + } + + module.exports = { +@@ -1733,9 +1762,9 @@ module.exports = { + getUserId: async (userId) => { + // check m2m user id + if (userId === config.m2m.M2M_AUDIT_USER_ID) { +- return config.m2m.M2M_AUDIT_USER_ID; ++ return config.m2m.M2M_AUDIT_USER_ID + } +- return ensureUbahnUserId({ userId }); ++ return ensureUbahnUserId({ userId }) + }, + getUserByExternalId, + getM2MToken, +@@ -1769,5 +1798,5 @@ module.exports = { + extractWorkPeriods, + getUserByHandle, + substituteStringByObject, +- createProject, +-}; ++ createProject ++} +diff --git a/src/controllers/RoleController.js b/src/controllers/RoleController.js +new file mode 100644 +index 0000000..747cbe4 +--- /dev/null ++++ b/src/controllers/RoleController.js +@@ -0,0 +1,59 @@ ++/** ++ * Controller for Role endpoints ++ */ ++const HttpStatus = require('http-status-codes') ++const service = require('../services/RoleService') ++ ++/** ++ * Get role by id ++ * @param req the request ++ * @param res the response ++ */ ++async function getRole (req, res) { ++ res.send(await service.getRole(req.authUser, req.params.id, req.query.fromDb)) ++} ++ ++/** ++ * Create role ++ * @param req the request ++ * @param res the response ++ */ ++async function createRole (req, res) { ++ res.send(await service.createRole(req.authUser, req.body)) ++} ++ ++/** ++ * update role by id ++ * @param req the request ++ * @param res the response ++ */ ++async function updateRole (req, res) { ++ res.send(await service.updateRole(req.authUser, req.params.id, req.body)) ++} ++ ++/** ++ * Delete role by id ++ * @param req the request ++ * @param res the response ++ */ ++async function deleteRole (req, res) { ++ await service.deleteRole(req.authUser, req.params.id) ++ res.status(HttpStatus.NO_CONTENT).end() ++} ++ ++/** ++ * Search roles ++ * @param req the request ++ * @param res the response ++ */ ++async function searchRoles (req, res) { ++ res.send(await service.searchRoles(req.authUser, req.query)) ++} ++ ++module.exports = { ++ getRole, ++ createRole, ++ updateRole, ++ deleteRole, ++ searchRoles ++} +diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js +index ca4f1bc..26d7073 100644 +--- a/src/controllers/TeamController.js ++++ b/src/controllers/TeamController.js +@@ -1,19 +1,19 @@ + /** + * Controller for TaaS teams endpoints + */ +-const HttpStatus = require('http-status-codes'); +-const service = require('../services/TeamService'); +-const helper = require('../common/helper'); ++const HttpStatus = require('http-status-codes') ++const service = require('../services/TeamService') ++const helper = require('../common/helper') + + /** + * Search teams + * @param req the request + * @param res the response + */ +-async function searchTeams(req, res) { +- const result = await service.searchTeams(req.authUser, req.query); +- helper.setResHeaders(req, res, result); +- res.send(result.result); ++async function searchTeams (req, res) { ++ const result = await service.searchTeams(req.authUser, req.query) ++ helper.setResHeaders(req, res, result) ++ res.send(result.result) + } + + /** +@@ -21,8 +21,8 @@ async function searchTeams(req, res) { + * @param req the request + * @param res the response + */ +-async function getTeam(req, res) { +- res.send(await service.getTeam(req.authUser, req.params.id)); ++async function getTeam (req, res) { ++ res.send(await service.getTeam(req.authUser, req.params.id)) + } + + /** +@@ -30,10 +30,10 @@ async function getTeam(req, res) { + * @param req the request + * @param res the response + */ +-async function getTeamJob(req, res) { ++async function getTeamJob (req, res) { + res.send( + await service.getTeamJob(req.authUser, req.params.id, req.params.jobId) +- ); ++ ) + } + + /** +@@ -41,9 +41,9 @@ async function getTeamJob(req, res) { + * @param req the request + * @param res the response + */ +-async function sendEmail(req, res) { +- await service.sendEmail(req.authUser, req.body); +- res.status(HttpStatus.NO_CONTENT).end(); ++async function sendEmail (req, res) { ++ await service.sendEmail(req.authUser, req.body) ++ res.status(HttpStatus.NO_CONTENT).end() + } + + /** +@@ -51,10 +51,10 @@ async function sendEmail(req, res) { + * @param req the request + * @param res the response + */ +-async function addMembers(req, res) { ++async function addMembers (req, res) { + res.send( + await service.addMembers(req.authUser, req.params.id, req.query, req.body) +- ); ++ ) + } + + /** +@@ -62,13 +62,13 @@ async function addMembers(req, res) { + * @param req the request + * @param res the response + */ +-async function searchMembers(req, res) { ++async function searchMembers (req, res) { + const result = await service.searchMembers( + req.authUser, + req.params.id, + req.query +- ); +- res.send(result.result); ++ ) ++ res.send(result.result) + } + + /** +@@ -76,13 +76,13 @@ async function searchMembers(req, res) { + * @param req the request + * @param res the response + */ +-async function searchInvites(req, res) { ++async function searchInvites (req, res) { + const result = await service.searchInvites( + req.authUser, + req.params.id, + req.query +- ); +- res.send(result.result); ++ ) ++ res.send(result.result) + } + + /** +@@ -90,13 +90,13 @@ async function searchInvites(req, res) { + * @param req the request + * @param res the response + */ +-async function deleteMember(req, res) { ++async function deleteMember (req, res) { + await service.deleteMember( + req.authUser, + req.params.id, + req.params.projectMemberId +- ); +- res.status(HttpStatus.NO_CONTENT).end(); ++ ) ++ res.status(HttpStatus.NO_CONTENT).end() + } + + /** +@@ -104,8 +104,8 @@ async function deleteMember(req, res) { + * @param req the request + * @param res the response + */ +-async function getMe(req, res) { +- res.send(await service.getMe(req.authUser)); ++async function getMe (req, res) { ++ res.send(await service.getMe(req.authUser)) + } + + /** +@@ -113,8 +113,8 @@ async function getMe(req, res) { + * @param req the request + * @param res the response + */ +-async function createProj(req, res) { +- res.send(await service.createProj(req.authUser, req.body)); ++async function createProj (req, res) { ++ res.send(await service.createProj(req.authUser, req.body)) + } + + module.exports = { +@@ -127,5 +127,5 @@ module.exports = { + searchInvites, + deleteMember, + getMe, +- createProj, +-}; ++ createProj ++} +diff --git a/src/eventHandlers/RoleEventHandler.js b/src/eventHandlers/RoleEventHandler.js +new file mode 100644 +index 0000000..38dbdb7 +--- /dev/null ++++ b/src/eventHandlers/RoleEventHandler.js +@@ -0,0 +1,64 @@ ++/* ++ * Handle events for ResourceBooking. ++ */ ++ ++const { Op } = require('sequelize') ++const _ = require('lodash') ++const models = require('../models') ++const logger = require('../common/logger') ++const helper = require('../common/helper') ++const JobService = require('../services/JobService') ++ ++const Job = models.Job ++ ++/** ++ * When a Role is deleted, jobs related to ++ * that role should be updated ++ * @param {object} payload the event payload ++ * @returns {undefined} ++ */ ++async function updateJobs (payload) { ++ // find jobs have this role ++ const jobs = await Job.findAll({ ++ where: { ++ roleIds: { [Op.contains]: [payload.value.id] } ++ }, ++ raw: true ++ }) ++ if (jobs.length === 0) { ++ logger.debug({ ++ component: 'RoleEventHandler', ++ context: 'updateJobs', ++ message: `id: ${payload.value.id} role has no related job - ignored` ++ }) ++ return ++ } ++ const m2mUser = helper.getAuditM2Muser() ++ // remove role id from related jobs ++ await Promise.all(_.map(jobs, async job => { ++ let roleIds = _.filter(job.roleIds, roleId => roleId !== payload.value.id) ++ if (roleIds.length === 0) { ++ roleIds = null ++ } ++ await JobService.partiallyUpdateJob(m2mUser, job.id, { roleIds }) ++ })) ++ logger.debug({ ++ component: 'RoleEventHandler', ++ context: 'updateJobs', ++ message: `role id: ${payload.value.id} removed from jobs with id: ${_.map(jobs, 'id')}` ++ }) ++} ++ ++/** ++ * Process role delete event. ++ * ++ * @param {Object} payload the event payload ++ * @returns {undefined} ++ */ ++async function processDelete (payload) { ++ await updateJobs(payload) ++} ++ ++module.exports = { ++ processDelete ++} +diff --git a/src/eventHandlers/index.js b/src/eventHandlers/index.js +index 1744599..6e0ec2a 100644 +--- a/src/eventHandlers/index.js ++++ b/src/eventHandlers/index.js +@@ -8,6 +8,7 @@ const JobEventHandler = require('./JobEventHandler') + const JobCandidateEventHandler = require('./JobCandidateEventHandler') + const ResourceBookingEventHandler = require('./ResourceBookingEventHandler') + const InterviewEventHandler = require('./InterviewEventHandler') ++const RoleEventHandler = require('./RoleEventHandler') + const logger = require('../common/logger') + + const TopicOperationMapping = { +@@ -16,7 +17,8 @@ const TopicOperationMapping = { + [config.TAAS_RESOURCE_BOOKING_CREATE_TOPIC]: ResourceBookingEventHandler.processCreate, + [config.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC]: ResourceBookingEventHandler.processUpdate, + [config.TAAS_RESOURCE_BOOKING_DELETE_TOPIC]: ResourceBookingEventHandler.processDelete, +- [config.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewEventHandler.processRequest ++ [config.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewEventHandler.processRequest, ++ [config.TAAS_ROLE_DELETE_TOPIC]: RoleEventHandler.processDelete + } + + /** +diff --git a/src/models/Job.js b/src/models/Job.js +index 49d34ff..66f15b0 100644 +--- a/src/models/Job.js ++++ b/src/models/Job.js +@@ -104,6 +104,12 @@ module.exports = (sequelize) => { + defaultValue: false, + allowNull: false + }, ++ roleIds: { ++ field: 'role_ids', ++ type: Sequelize.ARRAY({ ++ type: Sequelize.UUID ++ }) ++ }, + createdBy: { + field: 'created_by', + type: Sequelize.UUID, +diff --git a/src/models/Role.js b/src/models/Role.js +new file mode 100644 +index 0000000..57cd502 +--- /dev/null ++++ b/src/models/Role.js +@@ -0,0 +1,165 @@ ++const { Sequelize, Model } = require('sequelize') ++const config = require('config') ++const errors = require('../common/errors') ++ ++module.exports = (sequelize) => { ++ class Role extends Model { ++ /** ++ * Get role by id ++ * @param {String} id the role id ++ * @returns {Role} the role instance ++ */ ++ static async findById (id) { ++ const role = await Role.findOne({ ++ where: { ++ id ++ } ++ }) ++ if (!role) { ++ throw new errors.NotFoundError(`id: ${id} "Role" doesn't exists.`) ++ } ++ return role ++ } ++ } ++ Role.init( ++ { ++ id: { ++ type: Sequelize.UUID, ++ primaryKey: true, ++ allowNull: false, ++ defaultValue: Sequelize.UUIDV4 ++ }, ++ name: { ++ type: Sequelize.STRING(50), ++ allowNull: false ++ }, ++ description: { ++ type: Sequelize.STRING(1000) ++ }, ++ listOfSkills: { ++ field: 'list_of_skills', ++ type: Sequelize.ARRAY({ ++ type: Sequelize.STRING(50) ++ }) ++ }, ++ rates: { ++ type: Sequelize.ARRAY({ ++ type: Sequelize.JSONB({ ++ global: { ++ type: Sequelize.SMALLINT, ++ allowNull: false ++ }, ++ inCountry: { ++ field: 'in_country', ++ type: Sequelize.SMALLINT, ++ allowNull: false ++ }, ++ offShore: { ++ field: 'off_shore', ++ type: Sequelize.SMALLINT, ++ allowNull: false ++ }, ++ rate30Global: { ++ field: 'rate30_global', ++ type: Sequelize.SMALLINT ++ }, ++ rate30InCountry: { ++ field: 'rate30_in_country', ++ type: Sequelize.SMALLINT ++ }, ++ rate30OffShore: { ++ field: 'rate30_off_shore', ++ type: Sequelize.SMALLINT ++ }, ++ rate20Global: { ++ field: 'rate20_global', ++ type: Sequelize.SMALLINT ++ }, ++ rate20InCountry: { ++ field: 'rate20_in_country', ++ type: Sequelize.SMALLINT ++ }, ++ rate20OffShore: { ++ field: 'rate20_off_shore', ++ type: Sequelize.SMALLINT ++ } ++ }), ++ allowNull: false ++ }), ++ allowNull: false ++ }, ++ numberOfMembers: { ++ field: 'number_of_members', ++ type: Sequelize.NUMERIC ++ }, ++ numberOfMembersAvailable: { ++ field: 'number_of_members_available', ++ type: Sequelize.SMALLINT ++ }, ++ imageUrl: { ++ field: 'image_url', ++ type: Sequelize.STRING(255) ++ }, ++ timeToCandidate: { ++ field: 'time_to_candidate', ++ type: Sequelize.SMALLINT ++ }, ++ timeToInterview: { ++ field: 'time_to_interview', ++ type: Sequelize.SMALLINT ++ }, ++ createdBy: { ++ field: 'created_by', ++ type: Sequelize.UUID, ++ allowNull: false ++ }, ++ updatedBy: { ++ field: 'updated_by', ++ type: Sequelize.UUID ++ }, ++ createdAt: { ++ field: 'created_at', ++ type: Sequelize.DATE ++ }, ++ updatedAt: { ++ field: 'updated_at', ++ type: Sequelize.DATE ++ }, ++ deletedAt: { ++ field: 'deleted_at', ++ type: Sequelize.DATE ++ } ++ }, ++ { ++ schema: config.DB_SCHEMA_NAME, ++ sequelize, ++ tableName: 'roles', ++ paranoid: true, ++ deletedAt: 'deletedAt', ++ createdAt: 'createdAt', ++ updatedAt: 'updatedAt', ++ timestamps: true, ++ defaultScope: { ++ attributes: { ++ exclude: ['deletedAt'] ++ } ++ }, ++ hooks: { ++ afterCreate: (role) => { ++ delete role.dataValues.deletedAt ++ } ++ }, ++ indexes: [ ++ { ++ unique: true, ++ fields: ['name'], ++ where: { ++ deleted_at: null ++ } ++ } ++ ] ++ } ++ ) ++ ++ return Role ++} +diff --git a/src/routes/RoleRoutes.js b/src/routes/RoleRoutes.js +new file mode 100644 +index 0000000..2fb6d55 +--- /dev/null ++++ b/src/routes/RoleRoutes.js +@@ -0,0 +1,41 @@ ++/** ++ * Contains role routes ++ */ ++const constants = require('../../app-constants') ++ ++module.exports = { ++ '/roles': { ++ post: { ++ controller: 'RoleController', ++ method: 'createRole', ++ auth: 'jwt', ++ scopes: [constants.Scopes.CREATE_ROLE, constants.Scopes.ALL_ROLE] ++ }, ++ get: { ++ controller: 'RoleController', ++ method: 'searchRoles', ++ auth: 'jwt', ++ scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] ++ } ++ }, ++ '/roles/:id': { ++ get: { ++ controller: 'RoleController', ++ method: 'getRole', ++ auth: 'jwt', ++ scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] ++ }, ++ patch: { ++ controller: 'RoleController', ++ method: 'updateRole', ++ auth: 'jwt', ++ scopes: [constants.Scopes.UPDATE_ROLE, constants.Scopes.ALL_ROLE] ++ }, ++ delete: { ++ controller: 'RoleController', ++ method: 'deleteRole', ++ auth: 'jwt', ++ scopes: [constants.Scopes.DELETE_ROLE, constants.Scopes.ALL_ROLE] ++ } ++ } ++} +diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js +index 9bbe25c..07d777d 100644 +--- a/src/routes/TeamRoutes.js ++++ b/src/routes/TeamRoutes.js +@@ -1,7 +1,7 @@ + /** + * Contains taas team routes + */ +-const constants = require('../../app-constants'); ++const constants = require('../../app-constants') + + module.exports = { + '/taas-teams': { +@@ -9,85 +9,85 @@ module.exports = { + controller: 'TeamController', + method: 'searchTeams', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/email': { + post: { + controller: 'TeamController', + method: 'sendEmail', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/skills': { + get: { + controller: 'SkillController', + method: 'searchSkills', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/me': { + get: { + controller: 'TeamController', + method: 'getMe', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/:id': { + get: { + controller: 'TeamController', + method: 'getTeam', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/:id/jobs/:jobId': { + get: { + controller: 'TeamController', + method: 'getTeamJob', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/:id/members': { + post: { + controller: 'TeamController', + method: 'addMembers', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], ++ scopes: [constants.Scopes.READ_TAAS_TEAM] + }, + get: { + controller: 'TeamController', + method: 'searchMembers', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/:id/invites': { + get: { + controller: 'TeamController', + method: 'searchInvites', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/:id/members/:projectMemberId': { + delete: { + controller: 'TeamController', + method: 'deleteMember', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/createTeamRequest': { + post: { + controller: 'TeamController', + method: 'createProj', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, +- }, +-}; ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } ++ } ++} +diff --git a/src/services/InterviewService.js b/src/services/InterviewService.js +index 10a065f..a69a788 100644 +--- a/src/services/InterviewService.js ++++ b/src/services/InterviewService.js +@@ -241,8 +241,8 @@ async function requestInterview (currentUser, jobCandidateId, interview) { + const guestMembers = await helper.getMemberDetailsByEmails(interview.guestEmails) + interview.hostName = `${hostMembers[0].firstName} ${hostMembers[0].lastName}` + interview.guestNames = _.map(interview.guestEmails, (guestEmail) => { +- var foundGuestMember = _.find(guestMembers, function(guestMember) { return guestEmail == guestMember.email }); +- return (foundGuestMember != undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split("@")[0] ++ var foundGuestMember = _.find(guestMembers, function (guestMember) { return guestEmail === guestMember.email }) ++ return (foundGuestMember !== undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split('@')[0] + }) + + try { +diff --git a/src/services/JobService.js b/src/services/JobService.js +index 7d855bd..be5dfde 100644 +--- a/src/services/JobService.js ++++ b/src/services/JobService.js +@@ -74,6 +74,27 @@ async function _validateSkills (skills) { + } + } + ++/** ++ * Validate if all roles exist. ++ * ++ * @param {Array} roles the list of roles ++ * @returns {undefined} ++ */ ++async function _validateRoles (roles) { ++ const foundRolesObj = await models.Role.findAll({ ++ where: { ++ id: roles ++ }, ++ attributes: ['id'], ++ raw: true ++ }) ++ const foundRoles = _.map(foundRolesObj, 'id') ++ const nonexistentRoles = _.difference(roles, foundRoles) ++ if (nonexistentRoles.length > 0) { ++ throw new errors.BadRequestError(`Invalid roles: [${nonexistentRoles}]`) ++ } ++} ++ + /** + * Check user permission for getting job. + * +@@ -154,6 +175,10 @@ async function createJob (currentUser, job) { + } + + await _validateSkills(job.skills) ++ if (job.roleIds) { ++ job.roleIds = _.uniq(job.roleIds) ++ await _validateRoles(job.roleIds) ++ } + job.id = uuid() + job.createdBy = await helper.getUserId(currentUser.userId) + +@@ -177,7 +202,8 @@ createJob.schema = Joi.object().keys({ + rateType: Joi.rateType().allow(null), + workload: Joi.workload().allow(null), + skills: Joi.array().items(Joi.string().uuid()).required(), +- isApplicationPageActive: Joi.boolean() ++ isApplicationPageActive: Joi.boolean(), ++ roleIds: Joi.array().items(Joi.string().uuid().required()) + }).required() + }).required() + +@@ -192,6 +218,10 @@ async function updateJob (currentUser, id, data) { + if (data.skills) { + await _validateSkills(data.skills) + } ++ if (data.roleIds) { ++ data.roleIds = _.uniq(data.roleIds) ++ await _validateRoles(data.roleIds) ++ } + let job = await Job.findById(id) + const oldValue = job.toJSON() + +@@ -245,7 +275,8 @@ partiallyUpdateJob.schema = Joi.object().keys({ + rateType: Joi.rateType().allow(null), + workload: Joi.workload().allow(null), + skills: Joi.array().items(Joi.string().uuid()), +- isApplicationPageActive: Joi.boolean() ++ isApplicationPageActive: Joi.boolean(), ++ roleIds: Joi.array().items(Joi.string().uuid().required()).allow(null) + }).required() + }).required() + +@@ -276,7 +307,8 @@ fullyUpdateJob.schema = Joi.object().keys({ + workload: Joi.workload().allow(null).default(null), + skills: Joi.array().items(Joi.string().uuid()).required(), + status: Joi.jobStatus().default('sourcing'), +- isApplicationPageActive: Joi.boolean() ++ isApplicationPageActive: Joi.boolean(), ++ roleIds: Joi.array().items(Joi.string().uuid().required()).default(null) + }).required() + }).required() + +@@ -444,9 +476,9 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } + [Op.like]: `%${criteria.title}%` + } + } +- if (criteria.skills) { ++ if (criteria.skill) { + filter.skills = { +- [Op.contains]: [criteria.skills] ++ [Op.contains]: [criteria.skill] + } + } + const jobs = await Job.findAll({ +diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js +index f5c4020..fd3d777 100644 +--- a/src/services/ResourceBookingService.js ++++ b/src/services/ResourceBookingService.js +@@ -1,3 +1,4 @@ ++/* eslint-disable no-unreachable */ + /** + * This service provides operations of ResourceBooking. + */ +diff --git a/src/services/RoleService.js b/src/services/RoleService.js +new file mode 100644 +index 0000000..19006f6 +--- /dev/null ++++ b/src/services/RoleService.js +@@ -0,0 +1,305 @@ ++/** ++ * This service provides operations of Roles. ++ */ ++ ++const _ = require('lodash') ++const config = require('config') ++const Joi = require('joi') ++const { Op } = require('sequelize') ++const uuid = require('uuid') ++const helper = require('../common/helper') ++const logger = require('../common/logger') ++const errors = require('../common/errors') ++const models = require('../models') ++ ++const Role = models.Role ++const esClient = helper.getESClient() ++ ++/** ++ * Check user permission for deleting, creating or updating role. ++ * @param {Object} currentUser the user who perform this operation. ++ * @returns {undefined} ++ */ ++async function _checkUserPermissionForWriteDeleteRole (currentUser) { ++ if (!currentUser.hasManagePermission && !currentUser.isMachine) { ++ throw new errors.ForbiddenError('You are not allowed to perform this action!') ++ } ++} ++ ++/** ++ * Cleans and validates skill names using skills service ++ * @param {Array} skills array of skill names to validate ++ * @returns {undefined} ++ */ ++async function _cleanAndValidateSkillNames (skills) { ++ // remove duplicates, leading and trailing whitespaces, remove empties and convert to lowercase. ++ const cleanedSkills = _.uniq(_.filter(_.map(skills, skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill))) ++ if (cleanedSkills.length > 0) { ++ // search skills if they are exists ++ const { result } = await helper.getTopcoderSkills({ name: _.join(cleanedSkills, ',') }) ++ const skillNames = _.map(result, 'name') ++ // find skills that not valid ++ const unValidSkills = _.differenceWith(cleanedSkills, skillNames, (a, b) => _.toLower(a) === _.toLower(b)) ++ if (unValidSkills.length > 0) { ++ throw new errors.BadRequestError(`skills: "${unValidSkills}" are not valid`) ++ } ++ return cleanedSkills ++ } else { ++ return null ++ } ++} ++ ++/** ++ * Check user permission for deleting, creating or updating role. ++ * @param {Object} currentUser the user who perform this operation. ++ * @returns {undefined} ++ */ ++async function _checkIfSameNamedRoleExists (roleName) { ++ // We can't create another Role with the same name ++ const role = await Role.findOne({ ++ where: { ++ name: { [Op.iLike]: roleName } ++ }, ++ raw: true ++ }) ++ if (role) { ++ throw new errors.BadRequestError(`Role: "${role.name}" is already exists.`) ++ } ++} ++ ++/** ++ * Get role by id ++ * @param {Object} currentUser the user who perform this operation. ++ * @param {String} id the role id ++ * @param {Boolean} fromDb flag if query db for data or not ++ * @returns {Object} the role ++ */ ++async function getRole (currentUser, id, fromDb = false) { ++ if (!fromDb) { ++ try { ++ const role = await esClient.get({ ++ index: config.esConfig.ES_INDEX_ROLE, ++ id ++ }) ++ return { id: role.body._id, ...role.body._source } ++ } catch (err) { ++ if (helper.isDocumentMissingException(err)) { ++ throw new errors.NotFoundError(`id: ${id} "Role" not found`) ++ } ++ } ++ } ++ logger.info({ component: 'RoleService', context: 'getRole', message: 'try to query db for data' }) ++ const role = await Role.findById(id) ++ ++ return role.toJSON() ++} ++ ++getRole.schema = Joi.object().keys({ ++ currentUser: Joi.object().required(), ++ id: Joi.string().uuid().required(), ++ fromDb: Joi.boolean() ++}).required() ++ ++/** ++ * Create role ++ * @param {Object} currentUser the user who perform this operation ++ * @param {Object} role the role to be created ++ * @returns {Object} the created role ++ */ ++async function createRole (currentUser, role) { ++ // check permission ++ await _checkUserPermissionForWriteDeleteRole(currentUser) ++ // check if another Role with the same name exists. ++ await _checkIfSameNamedRoleExists(role.name) ++ // clean and validate skill names ++ if (role.listOfSkills) { ++ role.listOfSkills = await _cleanAndValidateSkillNames(role.listOfSkills) ++ } ++ ++ role.id = uuid.v4() ++ role.createdBy = await helper.getUserId(currentUser.userId) ++ ++ const created = await Role.create(role) ++ ++ await helper.postEvent(config.TAAS_ROLE_CREATE_TOPIC, created.toJSON()) ++ return created.toJSON() ++} ++ ++createRole.schema = Joi.object().keys({ ++ currentUser: Joi.object().required(), ++ role: Joi.object().keys({ ++ name: Joi.string().max(50).required(), ++ description: Joi.string().max(1000), ++ listOfSkills: Joi.array().items(Joi.string().max(50).required()), ++ rates: Joi.array().items(Joi.object().keys({ ++ global: Joi.smallint().required(), ++ inCountry: Joi.smallint().required(), ++ offShore: Joi.smallint().required(), ++ rate30Global: Joi.smallint(), ++ rate30InCountry: Joi.smallint(), ++ rate30OffShore: Joi.smallint(), ++ rate20Global: Joi.smallint(), ++ rate20InCountry: Joi.smallint(), ++ rate20OffShore: Joi.smallint() ++ }).required()).required(), ++ numberOfMembers: Joi.number(), ++ numberOfMembersAvailable: Joi.smallint(), ++ imageUrl: Joi.string().uri().max(255), ++ timeToCandidate: Joi.smallint(), ++ timeToInterview: Joi.smallint() ++ }).required() ++}).required() ++ ++/** ++ * Partially Update role ++ * @param {Object} currentUser the user who perform this operation ++ * @param {String} id the role id ++ * @param {Object} data the data to be updated ++ * @returns {Object} the updated role ++ */ ++async function updateRole (currentUser, id, data) { ++ // check permission ++ await _checkUserPermissionForWriteDeleteRole(currentUser) ++ ++ const role = await Role.findById(id) ++ const oldValue = role.toJSON() ++ // if name is changed, check if another Role with the same name exists. ++ if (data.name && data.name.toLowerCase() !== role.dataValues.name.toLowerCase()) { ++ await _checkIfSameNamedRoleExists(data.name) ++ } ++ // clean and validate skill names ++ if (data.listOfSkills) { ++ data.listOfSkills = await _cleanAndValidateSkillNames(data.listOfSkills) ++ } ++ ++ data.updatedBy = await helper.getUserId(currentUser.userId) ++ const updated = await role.update(data) ++ ++ await helper.postEvent(config.TAAS_ROLE_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) ++ return updated.toJSON() ++} ++ ++updateRole.schema = Joi.object().keys({ ++ currentUser: Joi.object().required(), ++ id: Joi.string().uuid().required(), ++ data: Joi.object().keys({ ++ name: Joi.string().max(50), ++ description: Joi.string().max(1000).allow(null), ++ listOfSkills: Joi.array().items(Joi.string().max(50).required()).allow(null), ++ rates: Joi.array().items(Joi.object().keys({ ++ global: Joi.smallint().required(), ++ inCountry: Joi.smallint().required(), ++ offShore: Joi.smallint().required(), ++ rate30Global: Joi.smallint(), ++ rate30InCountry: Joi.smallint(), ++ rate30OffShore: Joi.smallint(), ++ rate20Global: Joi.smallint(), ++ rate20InCountry: Joi.smallint(), ++ rate20OffShore: Joi.smallint() ++ }).required()), ++ numberOfMembers: Joi.number().allow(null), ++ numberOfMembersAvailable: Joi.smallint().allow(null), ++ imageUrl: Joi.string().uri().max(255).allow(null), ++ timeToCandidate: Joi.smallint().allow(null), ++ timeToInterview: Joi.smallint().allow(null) ++ }).required() ++}).required() ++ ++/** ++ * Delete role by id ++ * @param {Object} currentUser the user who perform this operation ++ * @param {String} id the role id ++ */ ++async function deleteRole (currentUser, id) { ++ // check permission ++ await _checkUserPermissionForWriteDeleteRole(currentUser) ++ ++ const role = await Role.findById(id) ++ await role.destroy() ++ await helper.postEvent(config.TAAS_ROLE_DELETE_TOPIC, { id }) ++} ++ ++deleteRole.schema = Joi.object().keys({ ++ currentUser: Joi.object().required(), ++ id: Joi.string().uuid().required() ++}).required() ++ ++/** ++ * List roles ++ * @param {Object} currentUser the user who perform this operation. ++ * @param {Object} criteria the search criteria ++ * @returns {Object} the search result ++ */ ++async function searchRoles (currentUser, criteria) { ++ // clean skill names and convert into an array ++ criteria.skillsList = _.filter(_.map(_.split(_.trim(criteria.skillsList), ','), skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill)) ++ try { ++ const esQuery = { ++ index: config.get('esConfig.ES_INDEX_ROLE'), ++ body: { ++ query: { ++ bool: { ++ must: [] ++ } ++ }, ++ size: 10000, ++ sort: [{ name: { order: 'asc' } }] ++ } ++ } ++ // Apply skill name filters. listOfSkills array should include all skills provided in criteria. ++ _.each(criteria.skillsList, skill => { ++ esQuery.body.query.bool.must.push({ ++ term: { ++ listOfSkills: skill ++ } ++ }) ++ }) ++ // Apply name filter, allow partial match ++ if (criteria.keyword) { ++ esQuery.body.query.bool.must.push({ ++ wildcard: { ++ name: `*${criteria.keyword}*` ++ ++ } ++ }) ++ } ++ logger.debug({ component: 'RoleService', context: 'searchRoles', message: `Query: ${JSON.stringify(esQuery)}` }) ++ ++ const { body } = await esClient.search(esQuery) ++ return _.map(body.hits.hits, (hit) => _.assign(hit._source, { id: hit._id })) ++ } catch (err) { ++ logger.logFullError(err, { component: 'RoleService', context: 'searchRoles' }) ++ } ++ logger.info({ component: 'RoleService', context: 'searchRoles', message: 'fallback to DB query' }) ++ const filter = { [Op.and]: [] } ++ // Apply skill name filters. listOfSkills array should include all skills provided in criteria. ++ if (criteria.skillsList) { ++ filter[Op.and].push({ listOfSkills: { [Op.contains]: criteria.skillsList } }) ++ } ++ // Apply name filter, allow partial match and ignore case ++ if (criteria.keyword) { ++ filter[Op.and].push({ name: { [Op.iLike]: `%${criteria.keyword}%` } }) ++ } ++ const queryCriteria = { ++ where: filter, ++ order: [['name', 'asc']] ++ } ++ const roles = await Role.findAll(queryCriteria) ++ return roles ++} ++ ++searchRoles.schema = Joi.object().keys({ ++ currentUser: Joi.object().required(), ++ criteria: Joi.object().keys({ ++ skillsList: Joi.string(), ++ keyword: Joi.string() ++ }).required() ++}).required() ++ ++module.exports = { ++ getRole, ++ createRole, ++ updateRole, ++ deleteRole, ++ searchRoles ++} +diff --git a/src/services/TeamService.js b/src/services/TeamService.js +index 3f6dbfd..4052e94 100644 +--- a/src/services/TeamService.js ++++ b/src/services/TeamService.js +@@ -2,16 +2,16 @@ + * This service provides operations of Job. + */ + +-const _ = require('lodash'); +-const Joi = require('joi'); +-const dateFNS = require('date-fns'); +-const config = require('config'); +-const emailTemplateConfig = require('../../config/email_template.config'); +-const helper = require('../common/helper'); +-const logger = require('../common/logger'); +-const errors = require('../common/errors'); +-const JobService = require('./JobService'); +-const ResourceBookingService = require('./ResourceBookingService'); ++const _ = require('lodash') ++const Joi = require('joi') ++const dateFNS = require('date-fns') ++const config = require('config') ++const emailTemplateConfig = require('../../config/email_template.config') ++const helper = require('../common/helper') ++const logger = require('../common/logger') ++const errors = require('../common/errors') ++const JobService = require('./JobService') ++const ResourceBookingService = require('./ResourceBookingService') + + const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { + return { +@@ -20,9 +20,9 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { + from: template.from, + recipients: template.recipients, + cc: template.cc, +- sendgridTemplateId: template.sendgridTemplateId, +- }; +-}); ++ sendgridTemplateId: template.sendgridTemplateId ++ } ++}) + + /** + * Function to get placed resource bookings with specific projectIds +@@ -30,14 +30,14 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { + * @param {Array} projectIds project ids + * @returns the request result + */ +-async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { +- const criteria = { status: 'placed', projectIds }; ++async function _getPlacedResourceBookingsByProjectIds (currentUser, projectIds) { ++ const criteria = { status: 'placed', projectIds } + const { result } = await ResourceBookingService.searchResourceBookings( + currentUser, + criteria, + { returnAll: true } +- ); +- return result; ++ ) ++ return result + } + + /** +@@ -46,13 +46,13 @@ async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { + * @param {Array} projectIds project ids + * @returns the request result + */ +-async function _getJobsByProjectIds(currentUser, projectIds) { ++async function _getJobsByProjectIds (currentUser, projectIds) { + const { result } = await JobService.searchJobs( + currentUser, + { projectIds }, + { returnAll: true } +- ); +- return result; ++ ) ++ return result + } + + /** +@@ -61,26 +61,26 @@ async function _getJobsByProjectIds(currentUser, projectIds) { + * @param {Object} criteria the search criteria + * @returns {Object} the search result, contain total/page/perPage and result array + */ +-async function searchTeams(currentUser, criteria) { +- const sort = `${criteria.sortBy} ${criteria.sortOrder}`; ++async function searchTeams (currentUser, criteria) { ++ const sort = `${criteria.sortBy} ${criteria.sortOrder}` + // Get projects from /v5/projects with searching criteria + const { + total, + page, + perPage, +- result: projects, ++ result: projects + } = await helper.getProjects(currentUser, { + page: criteria.page, + perPage: criteria.perPage, + name: criteria.name, +- sort, +- }); ++ sort ++ }) + return { + total, + page, + perPage, +- result: await getTeamDetail(currentUser, projects), +- }; ++ result: await getTeamDetail(currentUser, projects) ++ } + } + + searchTeams.schema = Joi.object() +@@ -107,13 +107,13 @@ searchTeams.schema = Joi.object() + then: Joi.forbidden().label( + 'sortOrder(with sortBy being `best match`)' + ), +- otherwise: Joi.string().valid('asc', 'desc').default('desc'), ++ otherwise: Joi.string().valid('asc', 'desc').default('desc') + }), +- name: Joi.string(), ++ name: Joi.string() + }) +- .required(), ++ .required() + }) +- .required(); ++ .required() + + /** + * Get team details +@@ -122,69 +122,69 @@ searchTeams.schema = Joi.object() + * @param {Object} isSearch the flag whether for search function + * @returns {Object} the search result + */ +-async function getTeamDetail(currentUser, projects, isSearch = true) { +- const projectIds = _.map(projects, 'id'); ++async function getTeamDetail (currentUser, projects, isSearch = true) { ++ const projectIds = _.map(projects, 'id') + // Get all placed resourceBookings filtered by projectIds + const resourceBookings = await _getPlacedResourceBookingsByProjectIds( + currentUser, + projectIds +- ); ++ ) + // Get all jobs filtered by projectIds +- const jobs = await _getJobsByProjectIds(currentUser, projectIds); ++ const jobs = await _getJobsByProjectIds(currentUser, projectIds) + + // Get first week day and last week day +- const curr = new Date(); +- const firstDay = dateFNS.startOfWeek(curr); +- const lastDay = dateFNS.endOfWeek(curr); ++ const curr = new Date() ++ const firstDay = dateFNS.startOfWeek(curr) ++ const lastDay = dateFNS.endOfWeek(curr) + + logger.debug({ + component: 'TeamService', + context: 'getTeamDetail', +- message: `week started: ${firstDay}, week ended: ${lastDay}`, +- }); ++ message: `week started: ${firstDay}, week ended: ${lastDay}` ++ }) + +- const result = []; ++ const result = [] + for (const project of projects) { +- const rbs = _.filter(resourceBookings, { projectId: project.id }); +- const res = _.clone(project); +- res.weeklyCost = 0; +- res.resources = []; ++ const rbs = _.filter(resourceBookings, { projectId: project.id }) ++ const res = _.clone(project) ++ res.weeklyCost = 0 ++ res.resources = [] + + if (rbs && rbs.length > 0) { + // Get minimal start date and maximal end date +- const startDates = []; +- const endDates = []; ++ const startDates = [] ++ const endDates = [] + for (const rbsItem of rbs) { + if (rbsItem.startDate) { +- startDates.push(new Date(rbsItem.startDate)); ++ startDates.push(new Date(rbsItem.startDate)) + } + if (rbsItem.endDate) { +- endDates.push(new Date(rbsItem.endDate)); ++ endDates.push(new Date(rbsItem.endDate)) + } + } + + if (startDates && startDates.length > 0) { +- res.startDate = _.min(startDates); ++ res.startDate = _.min(startDates) + } + if (endDates && endDates.length > 0) { +- res.endDate = _.max(endDates); ++ res.endDate = _.max(endDates) + } + + // Count weekly rate + for (const item of rbs) { + // ignore any resourceBooking that has customerRate missed + if (!item.customerRate) { +- continue; ++ continue + } +- const startDate = new Date(item.startDate); +- const endDate = new Date(item.endDate); ++ const startDate = new Date(item.startDate) ++ const endDate = new Date(item.endDate) + + // normally startDate is smaller than endDate for a resourceBooking so not check if startDate < endDate + if ( + (!item.startDate || startDate < lastDay) && + (!item.endDate || endDate > firstDay) + ) { +- res.weeklyCost += item.customerRate; ++ res.weeklyCost += item.customerRate + } + } + +@@ -194,48 +194,48 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { + const resource = { + id: rb.id, + userId: user.id, +- ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']), +- }; ++ ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']) ++ } + // If call function is not search, add jobId field + if (!isSearch) { +- resource.jobId = rb.jobId; +- resource.customerRate = rb.customerRate; +- resource.startDate = rb.startDate; +- resource.endDate = rb.endDate; ++ resource.jobId = rb.jobId ++ resource.customerRate = rb.customerRate ++ resource.startDate = rb.startDate ++ resource.endDate = rb.endDate + } +- return resource; +- }); ++ return resource ++ }) + }) +- ); ++ ) + if (resourceInfos && resourceInfos.length > 0) { +- res.resources = resourceInfos; ++ res.resources = resourceInfos + +- const userHandles = _.map(resourceInfos, 'handle'); ++ const userHandles = _.map(resourceInfos, 'handle') + // Get user photo from /v5/members +- const members = await helper.getMembers(userHandles); ++ const members = await helper.getMembers(userHandles) + + for (const item of res.resources) { + const findMember = _.find(members, { +- handleLower: item.handle.toLowerCase(), +- }); ++ handleLower: item.handle.toLowerCase() ++ }) + if (findMember && findMember.photoURL) { +- item.photo_url = findMember.photoURL; ++ item.photo_url = findMember.photoURL + } + } + } + } + +- const jobsTmp = _.filter(jobs, { projectId: project.id }); ++ const jobsTmp = _.filter(jobs, { projectId: project.id }) + if (jobsTmp && jobsTmp.length > 0) { + if (isSearch) { + // Count total positions +- res.totalPositions = 0; ++ res.totalPositions = 0 + for (const item of jobsTmp) { + // only sum numPositions of jobs whose status is NOT cancelled or closed + if (['cancelled', 'closed'].includes(item.status)) { +- continue; ++ continue + } +- res.totalPositions += item.numPositions; ++ res.totalPositions += item.numPositions + } + } else { + res.jobs = _.map(jobsTmp, (job) => { +@@ -249,15 +249,15 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { + 'skills', + 'customerRate', + 'status', +- 'title', +- ]); +- }); ++ 'title' ++ ]) ++ }) + } + } +- result.push(res); ++ result.push(res) + } + +- return result; ++ return result + } + + /** +@@ -266,35 +266,35 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { + * @param {String} id the job id + * @returns {Object} the team + */ +-async function getTeam(currentUser, id) { +- const project = await helper.getProjectById(currentUser, id); +- const result = await getTeamDetail(currentUser, [project], false); +- const teamDetail = result[0]; ++async function getTeam (currentUser, id) { ++ const project = await helper.getProjectById(currentUser, id) ++ const result = await getTeamDetail(currentUser, [project], false) ++ const teamDetail = result[0] + + // add job skills for result +- let jobSkills = []; ++ let jobSkills = [] + if (teamDetail && teamDetail.jobs) { + for (const job of teamDetail.jobs) { + if (job.skills) { +- const usersPromises = []; ++ const usersPromises = [] + _.map(job.skills, (skillId) => { +- usersPromises.push(helper.getSkillById(skillId)); +- }); +- jobSkills = await Promise.all(usersPromises); +- job.skills = jobSkills; ++ usersPromises.push(helper.getSkillById(skillId)) ++ }) ++ jobSkills = await Promise.all(usersPromises) ++ job.skills = jobSkills + } + } + } + +- return teamDetail; ++ return teamDetail + } + + getTeam.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), +- id: Joi.number().integer().required(), ++ id: Joi.number().integer().required() + }) +- .required(); ++ .required() + + /** + * Get team job with id +@@ -303,25 +303,25 @@ getTeam.schema = Joi.object() + * @param {String} jobId the job id + * @returns the team job + */ +-async function getTeamJob(currentUser, id, jobId) { +- const project = await helper.getProjectById(currentUser, id); +- const jobs = await _getJobsByProjectIds(currentUser, [project.id]); +- const job = _.find(jobs, { id: jobId }); ++async function getTeamJob (currentUser, id, jobId) { ++ const project = await helper.getProjectById(currentUser, id) ++ const jobs = await _getJobsByProjectIds(currentUser, [project.id]) ++ const job = _.find(jobs, { id: jobId }) + + if (!job) { + throw new errors.NotFoundError( + `id: ${jobId} "Job" with Team id ${id} doesn't exist` +- ); ++ ) + } + const result = { + id: job.id, +- title: job.title, +- }; ++ title: job.title ++ } + + if (job.skills) { + result.skills = await Promise.all( + _.map(job.skills, (skillId) => helper.getSkillById(skillId)) +- ); ++ ) + } + + // If the job has candidates, the following data for each candidate would be populated: +@@ -336,12 +336,12 @@ async function getTeamJob(currentUser, id, jobId) { + _.map(_.uniq(_.map(job.candidates, 'userId')), (userId) => + helper.getUserById(userId, true) + ) +- ); +- const userMap = _.groupBy(users, 'id'); ++ ) ++ const userMap = _.groupBy(users, 'id') + + // find photo URLs for users +- const members = await helper.getMembers(_.map(users, 'handle')); +- const photoURLMap = _.groupBy(members, 'handleLower'); ++ const members = await helper.getMembers(_.map(users, 'handle')) ++ const photoURLMap = _.groupBy(members, 'handleLower') + + result.candidates = _.map(job.candidates, (candidate) => { + const candidateData = _.pick(candidate, [ +@@ -349,33 +349,33 @@ async function getTeamJob(currentUser, id, jobId) { + 'resume', + 'userId', + 'interviews', +- 'id', +- ]); +- const userData = userMap[candidate.userId][0]; ++ 'id' ++ ]) ++ const userData = userMap[candidate.userId][0] + // attach user data to the candidate + Object.assign( + candidateData, + _.pick(userData, ['handle', 'firstName', 'lastName', 'skills']) +- ); ++ ) + // attach photo URL to the candidate +- const handleLower = userData.handle.toLowerCase(); ++ const handleLower = userData.handle.toLowerCase() + if (photoURLMap[handleLower]) { +- candidateData.photo_url = photoURLMap[handleLower][0].photoURL; ++ candidateData.photo_url = photoURLMap[handleLower][0].photoURL + } +- return candidateData; +- }); ++ return candidateData ++ }) + } + +- return result; ++ return result + } + + getTeamJob.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), +- jobId: Joi.string().guid().required(), ++ jobId: Joi.string().guid().required() + }) +- .required(); ++ .required() + + /** + * Send email through a particular template +@@ -383,21 +383,21 @@ getTeamJob.schema = Joi.object() + * @param {Object} data the email object + * @returns {undefined} + */ +-async function sendEmail(currentUser, data) { +- const template = emailTemplates[data.template]; +- const dataCC = data.cc || []; +- const templateCC = template.cc || []; +- const dataRecipients = data.recipients || []; +- const templateRecipients = template.recipients || []; ++async function sendEmail (currentUser, data) { ++ const template = emailTemplates[data.template] ++ const dataCC = data.cc || [] ++ const templateCC = template.cc || [] ++ const dataRecipients = data.recipients || [] ++ const templateRecipients = template.recipients || [] + const subjectBody = { + subject: data.subject || template.subject, +- body: data.body || template.body, +- }; ++ body: data.body || template.body ++ } + for (const key in subjectBody) { + subjectBody[key] = await helper.substituteStringByObject( + subjectBody[key], + data.data +- ); ++ ) + } + const emailData = { + // override template if coming data already have the 'from' address +@@ -407,9 +407,9 @@ async function sendEmail(currentUser, data) { + cc: _.uniq([...dataCC, ...templateCC]), + data: { ...data.data, ...subjectBody }, + sendgrid_template_id: template.sendgridTemplateId, +- version: 'v3', +- }; +- await helper.postEvent(config.EMAIL_TOPIC, emailData); ++ version: 'v3' ++ } ++ await helper.postEvent(config.EMAIL_TOPIC, emailData) + } + + sendEmail.schema = Joi.object() +@@ -423,11 +423,11 @@ sendEmail.schema = Joi.object() + data: Joi.object().required(), + from: Joi.string().email(), + recipients: Joi.array().items(Joi.string().email()).allow(null), +- cc: Joi.array().items(Joi.string().email()).allow(null), ++ cc: Joi.array().items(Joi.string().email()).allow(null) + }) +- .required(), ++ .required() + }) +- .required(); ++ .required() + + /** + * Add a member to a team as customer. +@@ -437,25 +437,25 @@ sendEmail.schema = Joi.object() + * @param {String} fields the fields to be returned + * @returns {Object} the member added + */ +-async function _addMemberToProjectAsCustomer(projectId, userId, fields) { ++async function _addMemberToProjectAsCustomer (projectId, userId, fields) { + try { + const member = await helper.createProjectMember( + projectId, + { userId: userId, role: 'customer' }, + { fields } +- ); +- return member; ++ ) ++ return member + } catch (err) { +- err.message = _.get(err, 'response.body.message') || err.message; ++ err.message = _.get(err, 'response.body.message') || err.message + if (err.message && err.message.includes('User already registered')) { +- throw new Error('User is already added'); ++ throw new Error('User is already added') + } + logger.error({ + component: 'TeamService', + context: '_addMemberToProjectAsCustomer', +- message: err.message, +- }); +- throw err; ++ message: err.message ++ }) ++ throw err + } + } + +@@ -467,16 +467,16 @@ async function _addMemberToProjectAsCustomer(projectId, userId, fields) { + * @param {Object} data the object including members with handle/email to be added + * @returns {Object} the success/failed added members + */ +-async function addMembers(currentUser, id, criteria, data) { +- await helper.getProjectById(currentUser, id); // check whether the user can access the project ++async function addMembers (currentUser, id, criteria, data) { ++ await helper.getProjectById(currentUser, id) // check whether the user can access the project + + const result = { + success: [], +- failed: [], +- }; ++ failed: [] ++ } + +- const handles = data.handles || []; +- const emails = data.emails || []; ++ const handles = data.handles || [] ++ const emails = data.emails || [] + + const handleMembers = await helper + .getMemberDetailsByHandles(handles) +@@ -484,9 +484,9 @@ async function addMembers(currentUser, id, criteria, data) { + _.map(members, (member) => ({ + ...member, + // populate members with lower-cased handle for case insensitive search +- handleLowerCase: member.handle.toLowerCase(), ++ handleLowerCase: member.handle.toLowerCase() + })) +- ); ++ ) + + const emailMembers = await helper + .getMemberDetailsByEmails(emails) +@@ -494,20 +494,20 @@ async function addMembers(currentUser, id, criteria, data) { + _.map(members, (member) => ({ + ...member, + // populate members with lower-cased email for case insensitive search +- emailLowerCase: member.email.toLowerCase(), ++ emailLowerCase: member.email.toLowerCase() + })) +- ); ++ ) + + await Promise.all([ + Promise.all( + handles.map((handle) => { + const memberDetails = _.find(handleMembers, { +- handleLowerCase: handle.toLowerCase(), +- }); ++ handleLowerCase: handle.toLowerCase() ++ }) + + if (!memberDetails) { +- result.failed.push({ error: "User doesn't exist", handle }); +- return; ++ result.failed.push({ error: "User doesn't exist", handle }) ++ return + } + + return _addMemberToProjectAsCustomer( +@@ -517,23 +517,23 @@ async function addMembers(currentUser, id, criteria, data) { + ) + .then((member) => { + // note, that we return `handle` in the same case it was in request +- result.success.push({ ...member, handle }); ++ result.success.push({ ...member, handle }) + }) + .catch((err) => { +- result.failed.push({ error: err.message, handle }); +- }); ++ result.failed.push({ error: err.message, handle }) ++ }) + }) + ), + + Promise.all( + emails.map((email) => { + const memberDetails = _.find(emailMembers, { +- emailLowerCase: email.toLowerCase(), +- }); ++ emailLowerCase: email.toLowerCase() ++ }) + + if (!memberDetails) { +- result.failed.push({ error: "User doesn't exist", email }); +- return; ++ result.failed.push({ error: "User doesn't exist", email }) ++ return + } + + return _addMemberToProjectAsCustomer( +@@ -543,16 +543,16 @@ async function addMembers(currentUser, id, criteria, data) { + ) + .then((member) => { + // note, that we return `email` in the same case it was in request +- result.success.push({ ...member, email }); ++ result.success.push({ ...member, email }) + }) + .catch((err) => { +- result.failed.push({ error: err.message, email }); +- }); ++ result.failed.push({ error: err.message, email }) ++ }) + }) +- ), +- ]); ++ ) ++ ]) + +- return result; ++ return result + } + + addMembers.schema = Joi.object() +@@ -561,18 +561,18 @@ addMembers.schema = Joi.object() + id: Joi.number().integer().required(), + criteria: Joi.object() + .keys({ +- fields: Joi.string(), ++ fields: Joi.string() + }) + .required(), + data: Joi.object() + .keys({ + handles: Joi.array().items(Joi.string()), +- emails: Joi.array().items(Joi.string().email()), ++ emails: Joi.array().items(Joi.string().email()) + }) + .or('handles', 'emails') +- .required(), ++ .required() + }) +- .required(); ++ .required() + + /** + * Search members in a team. +@@ -583,9 +583,9 @@ addMembers.schema = Joi.object() + * @params {Object} criteria the search criteria + * @returns {Object} the search result + */ +-async function searchMembers(currentUser, id, criteria) { +- const result = await helper.listProjectMembers(currentUser, id, criteria); +- return { result }; ++async function searchMembers (currentUser, id, criteria) { ++ const result = await helper.listProjectMembers(currentUser, id, criteria) ++ return { result } + } + + searchMembers.schema = Joi.object() +@@ -595,11 +595,11 @@ searchMembers.schema = Joi.object() + criteria: Joi.object() + .keys({ + role: Joi.string(), +- fields: Joi.string(), ++ fields: Joi.string() + }) +- .required(), ++ .required() + }) +- .required(); ++ .required() + + /** + * Search member invites for a team. +@@ -610,13 +610,13 @@ searchMembers.schema = Joi.object() + * @params {Object} criteria the search criteria + * @returns {Object} the search result + */ +-async function searchInvites(currentUser, id, criteria) { ++async function searchInvites (currentUser, id, criteria) { + const result = await helper.listProjectMemberInvites( + currentUser, + id, + criteria +- ); +- return { result }; ++ ) ++ return { result } + } + + searchInvites.schema = Joi.object() +@@ -625,11 +625,11 @@ searchInvites.schema = Joi.object() + id: Joi.number().integer().required(), + criteria: Joi.object() + .keys({ +- fields: Joi.string(), ++ fields: Joi.string() + }) +- .required(), ++ .required() + }) +- .required(); ++ .required() + + /** + * Remove a member from a team. +@@ -640,17 +640,17 @@ searchInvites.schema = Joi.object() + * @param {String} projectMemberId the id of the project member + * @returns {undefined} + */ +-async function deleteMember(currentUser, id, projectMemberId) { +- await helper.deleteProjectMember(currentUser, id, projectMemberId); ++async function deleteMember (currentUser, id, projectMemberId) { ++ await helper.deleteProjectMember(currentUser, id, projectMemberId) + } + + deleteMember.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), +- projectMemberId: Joi.number().integer().required(), ++ projectMemberId: Joi.number().integer().required() + }) +- .required(); ++ .required() + + /** + * Return details about the current user. +@@ -659,31 +659,31 @@ deleteMember.schema = Joi.object() + * @params {Object} criteria the search criteria + * @returns {Object} the user data for current user + */ +-async function getMe(currentUser) { +- return helper.getUserByExternalId(currentUser.userId); ++async function getMe (currentUser) { ++ return helper.getUserByExternalId(currentUser.userId) + } + + getMe.schema = Joi.object() + .keys({ +- currentUser: Joi.object().required(), ++ currentUser: Joi.object().required() + }) +- .required(); ++ .required() + + /** + * @param {Object} currentUser the user performing the operation. + * @param {Object} data project data + * @returns {Object} the created project + */ +-async function createProj(currentUser, data) { +- return helper.createProject(currentUser, data); ++async function createProj (currentUser, data) { ++ return helper.createProject(currentUser, data) + } + + createProj.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), +- data: Joi.object().required(), ++ data: Joi.object().required() + }) +- .required(); ++ .required() + + module.exports = { + searchTeams, +@@ -695,5 +695,5 @@ module.exports = { + searchInvites, + deleteMember, + getMe, +- createProj, +-}; ++ createProj ++} +-- +2.29.1.windows.1 + From 4d78351b8990bb4f053d003e107a8bc2a60d5faf Mon Sep 17 00:00:00 2001 From: eisbilir <72204071+eisbilir@users.noreply.github.com> Date: Mon, 31 May 2021 18:42:36 +0300 Subject: [PATCH 12/23] Delete taas-apis.patch --- taas-apis.patch | 9418 ----------------------------------------------- 1 file changed, 9418 deletions(-) delete mode 100644 taas-apis.patch diff --git a/taas-apis.patch b/taas-apis.patch deleted file mode 100644 index dcec96d8..00000000 --- a/taas-apis.patch +++ /dev/null @@ -1,9418 +0,0 @@ -From 79e73a98f25a56e793aaadbb9255bf3a99ecedd5 Mon Sep 17 00:00:00 2001 -From: eisbilir -Date: Sat, 29 May 2021 00:14:42 +0300 -Subject: [PATCH] role endpoint added - ---- - README.md | 6 +- - app-constants.js | 8 +- - config/default.js | 9 + - data/demo-data.json | 260 +- - ...coder-bookings-api.postman_collection.json | 3799 ++++++++++++++++- - docs/swagger.yaml | 476 +++ - ...topcoder-bookings.postman_environment.json | 56 +- - local/kafka-client/topics.txt | 3 + - migrations/2021-05-27-1-role-table-create.js | 146 + - .../2021-05-27-2-job-add-roleIds-field.js | 19 + - package.json | 1 + - scripts/data/exportData.js | 2 +- - scripts/data/importData.js | 2 +- - scripts/es/createIndex.js | 3 +- - scripts/es/deleteIndex.js | 3 +- - scripts/es/reIndexAll.js | 1 + - scripts/es/reIndexRoles.js | 37 + - src/bootstrap.js | 3 +- - src/common/helper.js | 1115 ++--- - src/controllers/RoleController.js | 59 + - src/controllers/TeamController.js | 62 +- - src/eventHandlers/RoleEventHandler.js | 64 + - src/eventHandlers/index.js | 4 +- - src/models/Job.js | 6 + - src/models/Role.js | 165 + - src/routes/RoleRoutes.js | 41 + - src/routes/TeamRoutes.js | 48 +- - src/services/InterviewService.js | 4 +- - src/services/JobService.js | 42 +- - src/services/ResourceBookingService.js | 1 + - src/services/RoleService.js | 305 ++ - src/services/TeamService.js | 390 +- - 32 files changed, 6271 insertions(+), 869 deletions(-) - create mode 100644 migrations/2021-05-27-1-role-table-create.js - create mode 100644 migrations/2021-05-27-2-job-add-roleIds-field.js - create mode 100644 scripts/es/reIndexRoles.js - create mode 100644 src/controllers/RoleController.js - create mode 100644 src/eventHandlers/RoleEventHandler.js - create mode 100644 src/models/Role.js - create mode 100644 src/routes/RoleRoutes.js - create mode 100644 src/services/RoleService.js - -diff --git a/README.md b/README.md -index 5e3895c..aa36c62 100644 ---- a/README.md -+++ b/README.md -@@ -87,6 +87,9 @@ - tc-taas-es-processor | [2021-04-09T21:20:19.035Z] app INFO : Starting kafka consumer - tc-taas-es-processor | 2021-04-09T21:20:21.292Z INFO no-kafka-client Joined group taas-es-processor generationId 1 as no-kafka-client-076538fc-60dd-4ca4-a2b9-520bdf73bc9e - tc-taas-es-processor | 2021-04-09T21:20:21.293Z INFO no-kafka-client Elected as group leader -+ tc-taas-es-processor | 2021-04-09T21:20:21.449Z DEBUG no-kafka-client Subscribed to taas.role.update:0 offset 0 leader kafka:9093 -+ tc-taas-es-processor | 2021-04-09T21:20:21.450Z DEBUG no-kafka-client Subscribed to taas.role.delete:0 offset 0 leader kafka:9093 -+ tc-taas-es-processor | 2021-04-09T21:20:21.451Z DEBUG no-kafka-client Subscribed to taas.role.requested:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-04-09T21:20:21.452Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.create:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-04-09T21:20:21.455Z DEBUG no-kafka-client Subscribed to taas.job.create:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-04-09T21:20:21.456Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.delete:0 offset 0 leader kafka:9093 -@@ -103,7 +106,7 @@ - tc-taas-es-processor | 2021-04-09T21:20:21.473Z DEBUG no-kafka-client Subscribed to taas.job.update:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-04-09T21:20:21.474Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.update:0 offset 0 leader kafka:9093 - tc-taas-es-processor | [2021-04-09T21:20:21.475Z] app INFO : Initialized....... -- tc-taas-es-processor | [2021-04-09T21:20:21.479Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete,taas.workperiod.create,taas.workperiod.update,taas.workperiod.delete,taas.workperiodpayment.create,taas.workperiodpayment.update,taas.workperiodpayment.delete -+ tc-taas-es-processor | [2021-04-09T21:20:21.479Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete,taas.workperiod.create,taas.workperiod.update,taas.workperiod.delete,taas.workperiodpayment.create,taas.workperiodpayment.update,taas.interview.requested,taas.interview.update,taas.interview.bulkUpdate,taas.role.requested,taas.role.update,taas.role.delete - tc-taas-es-processor | [2021-04-09T21:20:21.480Z] app INFO : Kick Start....... - tc-taas-es-processor | ********** Topcoder Health Check DropIn listening on port 3001 - tc-taas-es-processor | Topcoder Health Check DropIn started and ready to roll -@@ -194,6 +197,7 @@ To be able to change and test `taas-es-processor` locally you can follow the nex - | `npm run index:jobs ` | Indexes job data from db into ES, if jobId is not given all data is indexed. Use `-- --force` flag to skip confirmation | - | `npm run index:job-candidates ` | Indexes job candidate data from db into ES, if jobCandidateId is not given all data is indexed. Use `-- --force` flag to skip confirmation | - | `npm run index:resource-bookings ` | Indexes resource bookings data from db into ES, if resourceBookingsId is not given all data is indexed. Use `-- --force` flag to skip confirmation | -+| `npm run index:roles ` | Indexes roles data from db into ES, if roleId is not given all data is indexed. Use `-- --force` flag to skip confirmation | - | `npm run services:up` | Start services via docker-compose for local development. | - | `npm run services:down` | Stop services via docker-compose for local development. | - | `npm run services:logs -- -f ` | View logs of some service inside docker-compose. | -diff --git a/app-constants.js b/app-constants.js -index 534e46d..9b57772 100644 ---- a/app-constants.js -+++ b/app-constants.js -@@ -49,7 +49,13 @@ const Scopes = { - READ_INTERVIEW: 'read:taas-interviews', - CREATE_INTERVIEW: 'create:taas-interviews', - UPDATE_INTERVIEW: 'update:taas-interviews', -- ALL_INTERVIEW: 'all:taas-interviews' -+ ALL_INTERVIEW: 'all:taas-interviews', -+ // role -+ READ_ROLE: 'read:taas-roles', -+ CREATE_ROLE: 'create:taas-roles', -+ UPDATE_ROLE: 'update:taas-roles', -+ DELETE_ROLE: 'delete:taas-roles', -+ ALL_ROLE: 'all:taas-roles' - } - - // Interview related constants -diff --git a/config/default.js b/config/default.js -index 2b5ca7b..cf2a8a4 100644 ---- a/config/default.js -+++ b/config/default.js -@@ -76,6 +76,8 @@ module.exports = { - ES_INDEX_JOB_CANDIDATE: process.env.ES_INDEX_JOB_CANDIDATE || 'job_candidate', - // the resource booking index - ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking', -+ // the role index -+ ES_INDEX_ROLE: process.env.ES_INDEX_ROLE || 'role', - - // the max bulk size in MB for ES indexing - MAX_BULK_REQUEST_SIZE_MB: process.env.MAX_BULK_REQUEST_SIZE_MB || 20, -@@ -131,6 +133,13 @@ module.exports = { - TAAS_INTERVIEW_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_UPDATE_TOPIC || 'taas.interview.update', - // the interview bulk update Kafka message topic - TAAS_INTERVIEW_BULK_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_BULK_UPDATE_TOPIC || 'taas.interview.bulkUpdate', -+ // topics for role service -+ // the create role entity Kafka message topic -+ TAAS_ROLE_CREATE_TOPIC: process.env.TAAS_ROLE_CREATE_TOPIC || 'taas.role.requested', -+ // the update role entity Kafka message topic -+ TAAS_ROLE_UPDATE_TOPIC: process.env.TAAS_ROLE_UPDATE_TOPIC || 'taas.role.update', -+ // the delete role entity Kafka message topic -+ TAAS_ROLE_DELETE_TOPIC: process.env.TAAS_ROLE_DELETE_TOPIC || 'taas.role.delete', - - // the Kafka message topic for sending email - EMAIL_TOPIC: process.env.EMAIL_TOPIC || 'external.action.email', -diff --git a/data/demo-data.json b/data/demo-data.json -index e073344..5f6c4c0 100644 ---- a/data/demo-data.json -+++ b/data/demo-data.json -@@ -20,6 +20,7 @@ - ], - "status": "in-review", - "isApplicationPageActive": false, -+ "roleIds": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-09T21:21:10.394Z", -@@ -45,6 +46,7 @@ - ], - "status": "in-review", - "isApplicationPageActive": false, -+ "roleIds": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-09T21:11:26.934Z", -@@ -70,6 +72,7 @@ - ], - "status": "in-review", - "isApplicationPageActive": false, -+ "roleIds": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-09T21:23:18.595Z", -@@ -95,6 +98,7 @@ - ], - "status": "in-review", - "isApplicationPageActive": false, -+ "roleIds": null, - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-09T21:12:09.293Z", -@@ -181,18 +185,29 @@ - "interviews": [ - { - "id": "077aa2ca-5b60-4ad9-a965-1b37e08a5046", -+ "xaiId": null, - "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", -- "googleCalendarId": null, -- "customMessage": null, -- "xaiTemplate": "interview-30", -+ "calendarEventId": null, -+ "templateUrl": "interview-30", -+ "templateId": null, -+ "templateType": null, -+ "title": null, -+ "locationDetails": null, -+ "duration": null, - "round": 1, - "startTimestamp": null, -- "attendeesList": null, -+ "endTimestamp": null, -+ "hostName": null, -+ "hostEmail": null, -+ "guestNames": null, -+ "guestEmails": null, - "status": "Completed", -+ "rescheduleUrl": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:16:10.887Z", -- "updatedAt": "2021-05-09T21:16:10.887Z" -+ "updatedAt": "2021-05-09T21:16:10.887Z", -+ "deletedAt": null - } - ] - }, -@@ -210,33 +225,55 @@ - "interviews": [ - { - "id": "b1f7ba76-640f-47e2-9463-59e51b51ec60", -+ "xaiId": null, - "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", -- "googleCalendarId": "dummyId", -- "customMessage": "This is a custom message", -- "xaiTemplate": "interview-30", -+ "calendarEventId": null, -+ "templateUrl": "interview-30", -+ "templateId": null, -+ "templateType": null, -+ "title": null, -+ "locationDetails": null, -+ "duration": null, - "round": 2, - "startTimestamp": null, -- "attendeesList": null, -+ "endTimestamp": null, -+ "hostName": null, -+ "hostEmail": null, -+ "guestNames": null, -+ "guestEmails": null, - "status": "Scheduling", -+ "rescheduleUrl": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:17:23.517Z", -- "updatedAt": "2021-05-09T21:17:23.517Z" -+ "updatedAt": "2021-05-09T21:17:23.517Z", -+ "deletedAt": null - }, - { - "id": "3144fa65-ea1a-4bec-81b0-7cb1c8845826", -+ "xaiId": null, - "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", -- "googleCalendarId": null, -- "customMessage": null, -- "xaiTemplate": "interview-30", -+ "calendarEventId": null, -+ "templateUrl": "interview-30", -+ "templateId": null, -+ "templateType": null, -+ "title": null, -+ "locationDetails": null, -+ "duration": null, - "round": 1, - "startTimestamp": null, -- "attendeesList": null, -+ "endTimestamp": null, -+ "hostName": null, -+ "hostEmail": null, -+ "guestNames": null, -+ "guestEmails": null, - "status": "Completed", -+ "rescheduleUrl": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:16:39.019Z", -- "updatedAt": "2021-05-09T21:16:39.019Z" -+ "updatedAt": "2021-05-09T21:16:39.019Z", -+ "deletedAt": null - } - ] - }, -@@ -254,54 +291,81 @@ - "interviews": [ - { - "id": "976d23a9-5710-453f-99d9-f57a588bb610", -+ "xaiId": null, - "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", -- "googleCalendarId": "dummyId", -- "customMessage": "This is a custom message", -- "xaiTemplate": "interview-30", -+ "calendarEventId": null, -+ "templateUrl": "interview-30", -+ "templateId": null, -+ "templateType": null, -+ "title": null, -+ "locationDetails": null, -+ "duration": null, - "round": 3, - "startTimestamp": null, -- "attendeesList": [ -- "attendee1@yopmail.com", -- "attendee2@yopmail.com" -- ], -+ "endTimestamp": null, -+ "hostName": null, -+ "hostEmail": null, -+ "guestNames": null, -+ "guestEmails": null, - "status": "Scheduling", -+ "rescheduleUrl": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:21:28.713Z", -- "updatedAt": "2021-05-09T21:21:28.713Z" -+ "updatedAt": "2021-05-09T21:21:28.713Z", -+ "deletedAt": null - }, - { - "id": "a23e1bf2-1084-4cfe-a0d8-d83bc6fec655", -+ "xaiId": null, - "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", -- "googleCalendarId": "dummyId", -- "customMessage": "This is a custom message", -- "xaiTemplate": "interview-30", -+ "calendarEventId": null, -+ "templateUrl": "interview-30", -+ "templateId": null, -+ "templateType": null, -+ "title": null, -+ "locationDetails": null, -+ "duration": null, - "round": 2, - "startTimestamp": null, -- "attendeesList": [ -- "attendee1@yopmail.com", -- "attendee2@yopmail.com" -- ], -+ "endTimestamp": null, -+ "hostName": null, -+ "hostEmail": null, -+ "guestNames": null, -+ "guestEmails": null, - "status": "Scheduling", -+ "rescheduleUrl": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:21:22.428Z", -- "updatedAt": "2021-05-09T21:21:22.428Z" -+ "updatedAt": "2021-05-09T21:21:22.428Z", -+ "deletedAt": null - }, - { - "id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb", -+ "xaiId": null, - "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", -- "googleCalendarId": null, -- "customMessage": null, -- "xaiTemplate": "interview-30", -+ "calendarEventId": null, -+ "templateUrl": "interview-30", -+ "templateId": null, -+ "templateType": null, -+ "title": null, -+ "locationDetails": null, -+ "duration": null, - "round": 1, - "startTimestamp": null, -- "attendeesList": null, -+ "endTimestamp": null, -+ "hostName": null, -+ "hostEmail": null, -+ "guestNames": null, -+ "guestEmails": null, - "status": "Completed", -+ "rescheduleUrl": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:21:17.346Z", -- "updatedAt": "2021-05-09T21:21:17.346Z" -+ "updatedAt": "2021-05-09T21:21:17.346Z", -+ "deletedAt": null - } - ] - }, -@@ -2052,5 +2116,127 @@ - } - ] - } -+ ], -+ "Role": [ -+ { -+ "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.", -+ "listOfSkills": [ -+ "database", -+ "winforms", -+ "user interface (ui)", -+ "photoshop" -+ ], -+ "rates": [ -+ { -+ "global": 50, -+ "offShore": 10, -+ "inCountry": 20 -+ }, -+ { -+ "global": 25, -+ "offShore": 5, -+ "inCountry": 15 -+ } -+ ], -+ "numberOfMembers": "10", -+ "numberOfMembersAvailable": 8, -+ "imageUrl": "http://images.topcoder.com/member", -+ "timeToCandidate": 105, -+ "timeToInterview": 100, -+ "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", -+ "updatedBy": null, -+ "createdAt": "2021-05-27T21:43:08.201Z", -+ "updatedAt": "2021-05-27T21:43:08.201Z" -+ }, -+ { -+ "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.", -+ "listOfSkills": [ -+ "dropwizard", -+ "nginx", -+ "machine learning", -+ "force.com" -+ ], -+ "rates": [ -+ { -+ "global": 50, -+ "offShore": 10, -+ "inCountry": 20, -+ "rate20Global": 20, -+ "rate30Global": 20, -+ "rate20OffShore": 35, -+ "rate30OffShore": 35, -+ "rate20InCountry": 15, -+ "rate30InCountry": 15 -+ }, -+ { -+ "global": 25, -+ "offShore": 5, -+ "inCountry": 15, -+ "rate20Global": 20, -+ "rate30Global": 20, -+ "rate20OffShore": 35, -+ "rate30OffShore": 35, -+ "rate20InCountry": 15, -+ "rate30InCountry": 15 -+ } -+ ], -+ "numberOfMembers": "10", -+ "numberOfMembersAvailable": 8, -+ "imageUrl": "http://images.topcoder.com/member", -+ "timeToCandidate": 105, -+ "timeToInterview": 100, -+ "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", -+ "updatedBy": null, -+ "createdAt": "2021-05-27T21:43:04.717Z", -+ "updatedAt": "2021-05-27T21:43:04.717Z" -+ }, -+ { -+ "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.", -+ "listOfSkills": [ -+ "docker", -+ ".net", -+ "appcelerator", -+ "flux" -+ ], -+ "rates": [ -+ { -+ "global": 50, -+ "offShore": 10, -+ "inCountry": 20, -+ "rate20Global": 20, -+ "rate30Global": 20, -+ "rate20OffShore": 35, -+ "rate30OffShore": 35, -+ "rate20InCountry": 15, -+ "rate30InCountry": 15 -+ }, -+ { -+ "global": 25, -+ "offShore": 5, -+ "inCountry": 15, -+ "rate20Global": 20, -+ "rate30Global": 20, -+ "rate20OffShore": 35, -+ "rate30OffShore": 35, -+ "rate20InCountry": 15, -+ "rate30InCountry": 15 -+ } -+ ], -+ "numberOfMembers": "10", -+ "numberOfMembersAvailable": 6, -+ "imageUrl": "http://images.topcoder.com/member", -+ "timeToCandidate": 105, -+ "timeToInterview": 100, -+ "createdBy": "00000000-0000-0000-0000-000000000000", -+ "updatedBy": null, -+ "createdAt": "2021-05-27T21:43:09.342Z", -+ "updatedAt": "2021-05-27T21:43:09.342Z" -+ } - ] --} -+} -\ No newline at end of file -diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json -index a0518c5..96250f3 100644 ---- a/docs/Topcoder-bookings-api.postman_collection.json -+++ b/docs/Topcoder-bookings-api.postman_collection.json -@@ -1,6 +1,6 @@ - { - "info": { -- "_postman_id": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", -+ "_postman_id": "b0508e11-af20-4ea3-bfda-fec9f40ea531", - "name": "Topcoder-bookings-api", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, -@@ -17816,6 +17816,2993 @@ - } - ] - }, -+ { -+ "name": "Roles", -+ "item": [ -+ { -+ "name": "Create Role", -+ "item": [ -+ { -+ "name": "create role with admin", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ " if(pm.response.status === \"OK\"){\r", -+ " const response = pm.response.json()\r", -+ " pm.environment.set(\"roleId-1\", response.id);\r", -+ " }\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n }\n ],\n \"numberOfMembers\": 10,\n \"numberOfMembersAvailable\": 8,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with booking manager", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ " if(pm.response.status === \"OK\"){\r", -+ " const response = pm.response.json()\r", -+ " pm.environment.set(\"roleId-2\", response.id);\r", -+ " }\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_bookingManager}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Angular Developer\",\n \"description\": \"Angular is an open-source, client-side framework based on TypeScript and designed for building web applications.\",\n \"listOfSkills\": [\n \"Database\",\n \"Winforms\",\n \"User Interface (Ui)\",\n \"Photoshop\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"numberOfMembersAvailable\": 8,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with m2m create", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ " if(pm.response.status === \"OK\"){\r", -+ " const response = pm.response.json()\r", -+ " pm.environment.set(\"roleId-3\", response.id);\r", -+ " }\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_m2m_create_role}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Salesforce Developer\",\n \"description\": \"A Salesforce developer is a programmer who builds Salesforce applications across various PaaS (Platform as a Service) platforms.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\",\n \"appcelerator\",\n \"Flux\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n }\n ],\n \"numberOfMembers\": 10,\n \"numberOfMembersAvailable\": 6,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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 are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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 are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid token", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 401', function () {\r", -+ " pm.response.to.have.status(401);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer invalid_token" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with existent name", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"Role: \\\"Dev Ops Engineer\\\" is already exists.\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with missing parameter 1", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.name\\\" is required\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with missing parameter 2", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.rates\\\" is required\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with missing parameter 3", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.rates\\\" does not contain 1 required value(s)\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with missing parameter 4", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.rates[0].global\\\" is required\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with missing parameter 5", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.rates[0].inCountry\\\" is required\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with missing parameter 6", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.rates[0].offShore\\\" is required\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 1", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.name\\\" length must be less than or equal to 50 characters long\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 2", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.listOfSkills[0]\\\" length must be less than or equal to 50 characters long\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard\",\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 3", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.listOfSkills\\\" must be an array\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\":\"Dropwizard\",\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 4", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.rates\\\" must be an array\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 5", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.rates[0].global\\\" must be a number\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": \"first\",\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 6", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.rates[0].inCountry\\\" must be a number\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": \"fifty\",\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 7", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.numberOfMembers\\\" must be a number\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": null,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 8", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.imageUrl\\\" must be a valid uri\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 9", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"role.timeToCandidate\\\" must be less than or equal to 32767\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 99999,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 10", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"skills: \\\"teamworking,communication,problem-solving\\\" are not valid\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer 2\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 55,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ }, -+ { -+ "name": "Get Role", -+ "item": [ -+ { -+ "name": "get role with admin", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "get role with booking manager fromDb", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_bookingManager}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-2}}?fromDb=true", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-2}}" -+ ], -+ "query": [ -+ { -+ "key": "fromDb", -+ "value": "true" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "get role with m2m read", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_m2m_read_role}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-3}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-3}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "get role with connect user fromDb", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}?fromDb=true", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ], -+ "query": [ -+ { -+ "key": "fromDb", -+ "value": "true" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "get role with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-2}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-2}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "get role with invalid token", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 401', function () {\r", -+ " pm.response.to.have.status(401);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer invalid token" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-2}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-2}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "get role with invalid id", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"id\\\" must be a valid GUID\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/invalid", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "invalid" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "get role with missing id", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 404', function () {\r", -+ " pm.response.to.have.status(404);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"id: 00000000-0000-0000-0000-000000000000 \\\"Role\\\" not found\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "00000000-0000-0000-0000-000000000000" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "search roles with admin", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "search roles with booking manager", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_bookingManager}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles?skillsList=dropwizard, nginx,, machine learning , FORce.com &keyword=ops e", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ], -+ "query": [ -+ { -+ "key": "skillsList", -+ "value": "dropwizard, nginx,, machine learning , FORce.com " -+ }, -+ { -+ "key": "keyword", -+ "value": "ops e" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "search roles with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles?skillsList=dataBase, ,Photoshop&keyword=sale", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ], -+ "query": [ -+ { -+ "key": "skillsList", -+ "value": "dataBase, ,Photoshop" -+ }, -+ { -+ "key": "keyword", -+ "value": "sale" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "search roles with m2m read", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_m2m_read_role}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles?skillsList=DOCKER,.NET&keyword=dev", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ], -+ "query": [ -+ { -+ "key": "skillsList", -+ "value": "DOCKER,.NET" -+ }, -+ { -+ "key": "keyword", -+ "value": "dev" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "search roles with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles?keyword=dev", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ], -+ "query": [ -+ { -+ "key": "keyword", -+ "value": "dev" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "search roles with invalid token", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 401', function () {\r", -+ " pm.response.to.have.status(401);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer invalid token" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ }, -+ { -+ "name": "Update Role", -+ "item": [ -+ { -+ "name": "update role with admin", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer edit\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with booking manager", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_bookingManager}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Angular Developer edit\",\n \"description\": \"Angular is an open-source, client-side framework based on TypeScript and designed for building web applications.\",\n \"listOfSkills\": [\n \"Database\",\n \"Winforms\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-2}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-2}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with m2m update", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_m2m_update_role}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Salesforce Developer edit\",\n \"description\": \"A Salesforce developer is a programmer who builds Salesforce applications across various PaaS (Platform as a Service) platforms.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-3}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-3}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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 are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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 are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid token", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 401', function () {\r", -+ " pm.response.to.have.status(401);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer invalid_token" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid id", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"id\\\" must be a valid GUID\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/invalid", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "invalid" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with missing id", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 404', function () {\r", -+ " pm.response.to.have.status(404);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"id: 00000000-0000-0000-0000-000000000000 \\\"Role\\\" doesn't exists.\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "00000000-0000-0000-0000-000000000000" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with existent name", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"Role: \\\"Angular Developer edit\\\" is already exists.\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Angular Developer edit\"\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 1", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"data.name\\\" length must be less than or equal to 50 characters long\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 2", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"data.listOfSkills[0]\\\" length must be less than or equal to 50 characters long\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking\",\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 3", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"data.listOfSkills\\\" must be an array\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\":\"Teamworking\",\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 4", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"data.rates\\\" must be an array\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 5", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"data.rates[0].global\\\" must be a number\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": \"first\",\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 6", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"data.rates[0].inCountry\\\" must be a number\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": \"fifty\",\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 7", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"data.numberOfMembers\\\" must be a number\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": \"hundred\",\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 8", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"data.imageUrl\\\" must be a valid uri\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 9", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"data.timeToCandidate\\\" must be less than or equal to 32767\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 99999,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 10", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"skills: \\\"teamworking\\\" are not valid\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 66,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ }, -+ { -+ "name": "Delete Role", -+ "item": [ -+ { -+ "name": "delete role with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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 are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "delete role with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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 are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "delete role with invalid token", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 401', function () {\r", -+ " pm.response.to.have.status(401);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer invalid_token" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"teamworking\",\n \"communication\",\n \"problem-solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "delete role with invalid id", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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(\"\\\"id\\\" must be a valid GUID\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/invalid", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "invalid" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "delete role with missing id", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 404', function () {\r", -+ " pm.response.to.have.status(404);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"id: 00000000-0000-0000-0000-000000000000 \\\"Role\\\" doesn't exists.\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "00000000-0000-0000-0000-000000000000" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "delete role with admin", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 204', function () {\r", -+ " pm.response.to.have.status(204);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "delete role with booking manager", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 204', function () {\r", -+ " pm.response.to.have.status(204);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_bookingManager}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-2}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-2}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "delete role with m2m delete", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 204', function () {\r", -+ " pm.response.to.have.status(204);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_m2m_delete_role}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-3}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-3}}" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ } -+ ] -+ }, - { - "name": "health check", - "item": [ -@@ -22399,7 +25386,227 @@ - ], - "body": { - "mode": "raw", -- "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", -+ "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_by_administrator}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "work-period-payments", -+ "{{workPeriodPaymentId_created_by_administrator}}" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ }, -+ { -+ "name": "Roles", -+ "item": [ -+ { -+ "name": "✔ create role with admin", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ " if(pm.response.status === \"OK\"){\r", -+ " const response = pm.response.json()\r", -+ " pm.environment.set(\"roleId-1\", response.id);\r", -+ " }\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ get role with admin", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ search roles with admin", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ update role with admin", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer edit\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ delete role with admin", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 204', function () {\r", -+ " pm.response.to.have.status(204);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", - "options": { - "raw": { - "language": "json" -@@ -22407,13 +25614,13 @@ - } - }, - "url": { -- "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_by_administrator}}", -+ "raw": "{{URL}}/roles/{{roleId-1}}", - "host": [ - "{{URL}}" - ], - "path": [ -- "work-period-payments", -- "{{workPeriodPaymentId_created_by_administrator}}" -+ "roles", -+ "{{roleId-1}}" - ] - } - }, -@@ -24635,12 +27842,295 @@ - { - "key": "Authorization", - "type": "text", -- "value": "Bearer {{token_member_tester1234}}" -+ "value": "Bearer {{token_member_tester1234}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_for_member}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "work-period-payments", -+ "{{workPeriodPaymentId_created_for_member}}" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ }, -+ { -+ "name": "Roles", -+ "item": [ -+ { -+ "name": "Before Start", -+ "item": [ -+ { -+ "name": "✔ create role with admin", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ " if(pm.response.status === \"OK\"){\r", -+ " const response = pm.response.json()\r", -+ " pm.environment.set(\"roleId-1\", response.id);\r", -+ " }\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ }, -+ { -+ "name": "✘ create role with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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 are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ get role with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ search roles with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles?keyword=Dev", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ], -+ "query": [ -+ { -+ "key": "keyword", -+ "value": "Dev" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✘ update role with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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 are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✘ delete role with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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 are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" - } - ], - "body": { - "mode": "raw", -- "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", -+ "raw": "", - "options": { - "raw": { - "language": "json" -@@ -24648,13 +28138,13 @@ - } - }, - "url": { -- "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_for_member}}", -+ "raw": "{{URL}}/roles/{{roleId-1}}", - "host": [ - "{{URL}}" - ], - "path": [ -- "work-period-payments", -- "{{workPeriodPaymentId_created_for_member}}" -+ "roles", -+ "{{roleId-1}}" - ] - } - }, -@@ -26894,10 +30384,297 @@ - "response": [] - } - ] -+ }, -+ { -+ "name": "Roles", -+ "item": [ -+ { -+ "name": "Before Start", -+ "item": [ -+ { -+ "name": "✔ create role with admin", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ " if(pm.response.status === \"OK\"){\r", -+ " const response = pm.response.json()\r", -+ " pm.environment.set(\"roleId-1\", response.id);\r", -+ " }\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer 2\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ }, -+ { -+ "name": "✘ create role with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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 are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ get role with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ search roles with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 200', function () {\r", -+ " pm.response.to.have.status(200);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles?skillsList=Dropwizard, ,NGINX&keyword=Dev", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ], -+ "query": [ -+ { -+ "key": "skillsList", -+ "value": "Dropwizard, ,NGINX" -+ }, -+ { -+ "key": "keyword", -+ "value": "Dev" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✘ update role with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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 are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"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.\",\n \"listOfSkills\": [\n \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✘ delete role with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "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 are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] - } - ] - } - ] - } - ] --} -+} -\ No newline at end of file -diff --git a/docs/swagger.yaml b/docs/swagger.yaml -index a0b6064..e5f1ac2 100644 ---- a/docs/swagger.yaml -+++ b/docs/swagger.yaml -@@ -18,6 +18,8 @@ tags: - - name: ResourceBookings - - name: Teams - - name: WorkPeriods -+ - name: WorkPeriodPayments -+ - name: Roles - paths: - /jobs: - post: -@@ -3245,6 +3247,267 @@ paths: - application/json: - schema: - $ref: "#/components/schemas/Error" -+ /roles/new: -+ post: -+ tags: -+ - Roles -+ description: | -+ Create Role. -+ -+ **Authorization** Topcoder m2m token with create scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. -+ security: -+ - bearerAuth: [] -+ requestBody: -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/RoleRequestBody" -+ responses: -+ "200": -+ description: OK -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Role" -+ "400": -+ description: Bad request -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "401": -+ description: Not authenticated -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "403": -+ description: Forbidden -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "500": -+ description: Internal Server Error -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ /roles: -+ get: -+ tags: -+ - Roles -+ description: | -+ Search roles. -+ -+ **Authorization** Topcoder m2m token with read scope is allowed. Topcoder user token with any role is allowed. -+ security: -+ - bearerAuth: [] -+ parameters: -+ - in: query -+ name: skillsList -+ required: false -+ schema: -+ type: string -+ description: comma separated skill names. case-insensitive. -+ - in: query -+ name: keyword -+ required: false -+ schema: -+ type: string -+ description: role name. case-insensitive. partial match allowed -+ responses: -+ "200": -+ description: OK -+ content: -+ application/json: -+ schema: -+ type: array -+ items: -+ $ref: "#/components/schemas/Role" -+ "400": -+ description: Bad request -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "401": -+ description: Not authenticated -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "500": -+ description: Internal Server Error -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ /roles/{id}: -+ get: -+ tags: -+ - Roles -+ description: | -+ Get role by id. -+ -+ **Authorization** Topcoder m2m token with read scope is allowed. Topcoder user token with any role is allowed. -+ security: -+ - bearerAuth: [] -+ parameters: -+ - in: path -+ name: id -+ description: The role id. -+ required: true -+ schema: -+ type: string -+ format: uuid -+ - in: query -+ name: fromDb -+ description: get data from db or not. -+ required: false -+ schema: -+ type: boolean -+ default: false -+ responses: -+ "200": -+ description: OK -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Role" -+ "400": -+ description: Bad request -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "401": -+ description: Not authenticated -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "404": -+ description: Not Found -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "500": -+ description: Internal Server Error -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ patch: -+ tags: -+ - Roles -+ description: | -+ Partial Update role. -+ -+ **Authorization** Topcoder m2m token with update scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. -+ security: -+ - bearerAuth: [] -+ parameters: -+ - in: path -+ name: id -+ description: The id of role. -+ required: true -+ schema: -+ type: string -+ format: uuid -+ requestBody: -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/RolePatchRequestBody" -+ responses: -+ "200": -+ description: OK -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Role" -+ "400": -+ description: Bad request -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "401": -+ description: Not authenticated -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "403": -+ description: Forbidden -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "404": -+ description: Not Found -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "500": -+ description: Internal Server Error -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ delete: -+ tags: -+ - Roles -+ description: | -+ Delete the role. -+ -+ **Authorization** Topcoder m2m token with delete scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. -+ security: -+ - bearerAuth: [] -+ parameters: -+ - in: path -+ name: id -+ description: The id of role. -+ required: true -+ schema: -+ type: string -+ format: uuid -+ responses: -+ "204": -+ description: OK -+ "400": -+ description: Bad request -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "401": -+ description: Not authenticated -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "403": -+ description: Forbidden -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "404": -+ description: Not Found -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "500": -+ description: Internal Server Error -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" - /health: - get: - tags: -@@ -3335,6 +3598,13 @@ components: - type: string - format: uuid - description: "The skill id." -+ roleIds: -+ type: array -+ description: "The roles." -+ items: -+ type: string -+ format: uuid -+ description: "The role id." - status: - type: string - enum: ["sourcing", "in-review", "assigned", "closed", "cancelled"] -@@ -3424,6 +3694,13 @@ components: - type: string - format: uuid - description: "The skill id." -+ roleIds: -+ type: array -+ description: "The roles." -+ items: -+ type: string -+ format: uuid -+ description: "The role id." - isApplicationPageActive: - type: boolean - default: false -@@ -3865,6 +4142,13 @@ components: - type: string - format: uuid - description: "The skill id." -+ roleIds: -+ type: array -+ description: "The roles." -+ items: -+ type: string -+ format: uuid -+ description: "The role id." - isApplicationPageActive: - type: boolean - default: false -@@ -4710,6 +4994,198 @@ components: - type: string - description: "the email of a member" - example: "xxx@xxx.com" -+ Role: -+ required: -+ - id -+ - name -+ - rates -+ - createdAt -+ - createdBy -+ properties: -+ id: -+ type: string -+ format: uuid -+ description: "The role id." -+ name: -+ type: string -+ example: "Dev Ops Engineer" -+ description: "The role name." -+ description: -+ type: string -+ example: "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: "The role description" -+ listOfSkills: -+ type: array -+ description: "The array of skill names." -+ items: -+ type: string -+ example: "HTML" -+ description: "The skill name" -+ rates: -+ type: array -+ description: "The rates object array." -+ items: -+ $ref: "#/components/schemas/RoleRates" -+ numberOfMembers: -+ type: number -+ example: 100 -+ description: "The number of members." -+ numberOfMembersAvailable: -+ type: integer -+ example: 100 -+ description: "The number of members available." -+ imageUrl: -+ type: string -+ format: url -+ example: "http://images.topcoder.com/images" -+ description: "The image url of the role." -+ timeToCandidate: -+ type: integer -+ example: 200 -+ description: "The time to candidate." -+ timeToInterview: -+ type: integer -+ example: 300 -+ description: "The time to interview." -+ createdAt: -+ type: string -+ format: date-time -+ description: "The role created date." -+ createdBy: -+ type: string -+ format: uuid -+ description: "The user Id who created the role.(Will get the user info from the token)" -+ updatedAt: -+ type: string -+ format: date-time -+ description: "The role last updated at." -+ updatedBy: -+ type: string -+ format: uuid -+ description: "The user Id who updated the role last time.(Will get the user info from the token)" -+ RoleRequestBody: -+ required: -+ - name -+ - rates -+ properties: -+ name: -+ type: string -+ example: "Dev Ops Engineer" -+ description: "The role name." -+ description: -+ type: string -+ example: "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: "The role description" -+ listOfSkills: -+ type: array -+ description: "The array of skill names." -+ items: -+ type: string -+ example: "HTML" -+ description: "The skill name" -+ rates: -+ type: array -+ description: "The rates object array." -+ items: -+ $ref: "#/components/schemas/RoleRates" -+ numberOfMembers: -+ type: number -+ example: 100 -+ description: "The number of members." -+ numberOfMembersAvailable: -+ type: number -+ example: 100 -+ description: "The number of members available." -+ imageUrl: -+ type: string -+ format: url -+ example: "http://images.topcoder.com/images" -+ description: "The image url of the role." -+ timeToCandidate: -+ type: integer -+ example: 200 -+ description: "The time to candidate." -+ timeToInterview: -+ type: integer -+ example: 300 -+ description: "The time to interview." -+ RolePatchRequestBody: -+ properties: -+ name: -+ type: string -+ example: "Dev Ops Engineer" -+ description: "The role name." -+ description: -+ type: string -+ example: "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: "The role description" -+ listOfSkills: -+ type: array -+ description: "The array of skill names." -+ items: -+ type: string -+ example: "HTML" -+ description: "The skill name" -+ rates: -+ type: array -+ description: "The rates object array." -+ items: -+ $ref: "#/components/schemas/RoleRates" -+ numberOfMembers: -+ type: number -+ example: 100 -+ description: "The number of members." -+ numberOfMembersAvailable: -+ type: number -+ example: 100 -+ description: "The number of members available." -+ imageUrl: -+ type: string -+ format: url -+ example: "http://images.topcoder.com/images" -+ description: "The image url of the role." -+ timeToCandidate: -+ type: integer -+ example: 200 -+ description: "The time to candidate." -+ timeToInterview: -+ type: integer -+ example: 300 -+ description: "The time to interview." -+ RoleRates: -+ required: -+ - global -+ - inCountry -+ - offShore -+ type: object -+ properties: -+ global: -+ type: integer -+ example: 10 -+ inCountry: -+ type: integer -+ example: 20 -+ offShore: -+ type: integer -+ example: 30 -+ rate30Global: -+ type: integer -+ example: 10 -+ rate30InCountry: -+ type: integer -+ example: 20 -+ rate30OffShore: -+ type: integer -+ example: 30 -+ rate20Global: -+ type: integer -+ example: 10 -+ rate20InCountry: -+ type: integer -+ example: 20 -+ rate20OffShore: -+ type: integer -+ example: 30 - ProjectMember: - type: object - example: -diff --git a/docs/topcoder-bookings.postman_environment.json b/docs/topcoder-bookings.postman_environment.json -index 837b55d..c83fc9a 100644 ---- a/docs/topcoder-bookings.postman_environment.json -+++ b/docs/topcoder-bookings.postman_environment.json -@@ -1,5 +1,5 @@ - { -- "id": "228f4dcc-6914-462e-9b56-3285b643a2f8", -+ "id": "0ce42def-1c70-4c24-8986-914caa57f3c8", - "name": "topcoder-bookings", - "values": [ - { -@@ -312,11 +312,6 @@ - "value": "", - "enabled": true - }, -- { -- "key": "job_id_created_for_member", -- "value": "", -- "enabled": true -- }, - { - "key": "resource_bookings_id_created_for_member", - "value": "", -@@ -327,11 +322,6 @@ - "value": "", - "enabled": true - }, -- { -- "key": "job_id_created_for_connect_manager", -- "value": "", -- "enabled": true -- }, - { - "key": "resource_bookings_id_created_for_connect_manager", - "value": "", -@@ -461,9 +451,49 @@ - "key": "interview_id_created_for_connect_manager", - "value": "", - "enabled": true -+ }, -+ { -+ "key": "token_m2m_create_role", -+ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.f1QP1QTacyDxy7dwzUhBIT8blXCjKn_mnu9Cg59vIc8", -+ "enabled": true -+ }, -+ { -+ "key": "token_m2m_read_role", -+ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJyZWFkOnRhYXMtcm9sZXMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.ZeWS_W2o8YwlvIB_-z0CFFa9zhRjptCk7qNXsPPWxVY", -+ "enabled": true -+ }, -+ { -+ "key": "token_m2m_update_role", -+ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJ1cGRhdGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.0t4k0skZmxAUKuHQrG3ZrO2dgWcDMLD8W1rVluCy7XQ", -+ "enabled": true -+ }, -+ { -+ "key": "token_m2m_delete_role", -+ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJkZWxldGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.NSBbWOk5jCB8nIvLiZwJtR9px5wmUQaQjgpDlMDJ9hk", -+ "enabled": true -+ }, -+ { -+ "key": "token_m2m_all_role", -+ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJhbGw6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.cn0QVTOFnbHJckYqmGcpUBT8wQUxXWwtteWU7uhlDtI", -+ "enabled": true -+ }, -+ { -+ "key": "roleId-1", -+ "value": "", -+ "enabled": true -+ }, -+ { -+ "key": "roleId-2", -+ "value": "", -+ "enabled": true -+ }, -+ { -+ "key": "roleId-3", -+ "value": "", -+ "enabled": true - } - ], - "_postman_variable_scope": "environment", -- "_postman_exported_at": "2021-05-10T05:06:38.661Z", -- "_postman_exported_using": "Postman/8.3.1" -+ "_postman_exported_at": "2021-05-27T01:32:45.726Z", -+ "_postman_exported_using": "Postman/8.5.1" - } -\ No newline at end of file -diff --git a/local/kafka-client/topics.txt b/local/kafka-client/topics.txt -index 8766a1b..760c3a8 100644 ---- a/local/kafka-client/topics.txt -+++ b/local/kafka-client/topics.txt -@@ -3,16 +3,19 @@ taas.jobcandidate.create - taas.resourcebooking.create - taas.workperiod.create - taas.workperiodpayment.create -+taas.role.requested - taas.job.update - taas.jobcandidate.update - taas.resourcebooking.update - taas.workperiod.update - taas.workperiodpayment.update -+taas.role.update - taas.job.delete - taas.jobcandidate.delete - taas.resourcebooking.delete - taas.workperiod.delete - taas.workperiodpayment.delete -+taas.role.delete - taas.interview.requested - taas.interview.update - taas.interview.bulkUpdate -diff --git a/migrations/2021-05-27-1-role-table-create.js b/migrations/2021-05-27-1-role-table-create.js -new file mode 100644 -index 0000000..bce2ae1 ---- /dev/null -+++ b/migrations/2021-05-27-1-role-table-create.js -@@ -0,0 +1,146 @@ -+const config = require('config') -+ -+/* -+ * Create role table -+ */ -+ -+module.exports = { -+ up: async (queryInterface, Sequelize) => { -+ const transaction = await queryInterface.sequelize.transaction() -+ try { -+ await queryInterface.createTable('roles', { -+ id: { -+ type: Sequelize.UUID, -+ primaryKey: true, -+ allowNull: false, -+ defaultValue: Sequelize.UUIDV4 -+ }, -+ name: { -+ type: Sequelize.STRING(50), -+ allowNull: false -+ }, -+ description: { -+ type: Sequelize.STRING(1000) -+ }, -+ listOfSkills: { -+ field: 'list_of_skills', -+ type: Sequelize.ARRAY({ -+ type: Sequelize.STRING(50) -+ }) -+ }, -+ rates: { -+ type: Sequelize.ARRAY({ -+ type: Sequelize.JSONB({ -+ global: { -+ type: Sequelize.SMALLINT, -+ allowNull: false -+ }, -+ inCountry: { -+ field: 'in_country', -+ type: Sequelize.SMALLINT, -+ allowNull: false -+ }, -+ offShore: { -+ field: 'off_shore', -+ type: Sequelize.SMALLINT, -+ allowNull: false -+ }, -+ rate30Global: { -+ field: 'rate30_global', -+ type: Sequelize.SMALLINT -+ }, -+ rate30InCountry: { -+ field: 'rate30_in_country', -+ type: Sequelize.SMALLINT -+ }, -+ rate30OffShore: { -+ field: 'rate30_off_shore', -+ type: Sequelize.SMALLINT -+ }, -+ rate20Global: { -+ field: 'rate20_global', -+ type: Sequelize.SMALLINT -+ }, -+ rate20InCountry: { -+ field: 'rate20_in_country', -+ type: Sequelize.SMALLINT -+ }, -+ rate20OffShore: { -+ field: 'rate20_off_shore', -+ type: Sequelize.SMALLINT -+ } -+ }), -+ allowNull: false -+ }), -+ allowNull: false -+ }, -+ numberOfMembers: { -+ field: 'number_of_members', -+ type: Sequelize.NUMERIC -+ }, -+ numberOfMembersAvailable: { -+ field: 'number_of_members_available', -+ type: Sequelize.SMALLINT -+ }, -+ imageUrl: { -+ field: 'image_url', -+ type: Sequelize.STRING(255) -+ }, -+ timeToCandidate: { -+ field: 'time_to_candidate', -+ type: Sequelize.SMALLINT -+ }, -+ timeToInterview: { -+ field: 'time_to_interview', -+ type: Sequelize.SMALLINT -+ }, -+ createdBy: { -+ field: 'created_by', -+ type: Sequelize.UUID, -+ allowNull: false -+ }, -+ updatedBy: { -+ field: 'updated_by', -+ type: Sequelize.UUID -+ }, -+ createdAt: { -+ field: 'created_at', -+ type: Sequelize.DATE -+ }, -+ updatedAt: { -+ field: 'updated_at', -+ type: Sequelize.DATE -+ }, -+ deletedAt: { -+ field: 'deleted_at', -+ type: Sequelize.DATE -+ } -+ }, { -+ schema: config.DB_SCHEMA_NAME, -+ transaction -+ }) -+ await queryInterface.addIndex( -+ { -+ tableName: 'roles', -+ schema: config.DB_SCHEMA_NAME -+ }, -+ ['name'], -+ { -+ type: 'UNIQUE', -+ where: { deleted_at: null }, -+ transaction: transaction -+ } -+ ) -+ await transaction.commit() -+ } catch (err) { -+ await transaction.rollback() -+ throw err -+ } -+ }, -+ down: async (queryInterface, Sequelize) => { -+ await queryInterface.dropTable({ -+ tableName: 'roles', -+ schema: config.DB_SCHEMA_NAME -+ }) -+ } -+} -diff --git a/migrations/2021-05-27-2-job-add-roleIds-field.js b/migrations/2021-05-27-2-job-add-roleIds-field.js -new file mode 100644 -index 0000000..a5b9f4b ---- /dev/null -+++ b/migrations/2021-05-27-2-job-add-roleIds-field.js -@@ -0,0 +1,19 @@ -+const config = require('config') -+ -+/* -+ * Add roleIds field to the Job model. -+ */ -+ -+module.exports = { -+ up: async (queryInterface, Sequelize) => { -+ await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'role_ids', -+ { -+ type: Sequelize.ARRAY({ -+ type: Sequelize.UUID -+ }) -+ }) -+ }, -+ down: async (queryInterface, Sequelize) => { -+ await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'role_ids') -+ } -+} -diff --git a/package.json b/package.json -index 0fa24cc..510504f 100644 ---- a/package.json -+++ b/package.json -@@ -15,6 +15,7 @@ - "index:jobs": "node scripts/es/reIndexJobs.js", - "index:job-candidates": "node scripts/es/reIndexJobCandidates.js", - "index:resource-bookings": "node scripts/es/reIndexResourceBookings.js", -+ "index:roles": "node scripts/es/reIndexRoles.js", - "data:export": "node scripts/data/exportData.js", - "data:import": "node scripts/data/importData.js", - "migrate": "npx sequelize db:migrate", -diff --git a/scripts/data/exportData.js b/scripts/data/exportData.js -index 4eee1ad..cb61e58 100644 ---- a/scripts/data/exportData.js -+++ b/scripts/data/exportData.js -@@ -28,7 +28,7 @@ const resourceBookingModelOpts = { - - const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH - const userPrompt = `WARNING: are you sure you want to export all data in the database to a json file with the path ${filePath}? This will overwrite the file.` --const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts] -+const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts, 'Role'] - - async function exportData () { - await helper.promptUser(userPrompt, async () => { -diff --git a/scripts/data/importData.js b/scripts/data/importData.js -index 2e9c168..a0aeeb6 100644 ---- a/scripts/data/importData.js -+++ b/scripts/data/importData.js -@@ -28,7 +28,7 @@ const resourceBookingModelOpts = { - - const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH - const userPrompt = `WARNING: this would remove existing data. Are you sure you want to import data from a json file with the path ${filePath}?` --const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts] -+const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts, 'Role'] - - async function importData () { - await helper.promptUser(userPrompt, async () => { -diff --git a/scripts/es/createIndex.js b/scripts/es/createIndex.js -index d2c7294..269cd5a 100644 ---- a/scripts/es/createIndex.js -+++ b/scripts/es/createIndex.js -@@ -8,7 +8,8 @@ const helper = require('../../src/common/helper') - const indices = [ - config.get('esConfig.ES_INDEX_JOB'), - config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), -- config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') -+ config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), -+ config.get('esConfig.ES_INDEX_ROLE') - ] - const userPrompt = `WARNING: Are you sure want to create the following elasticsearch indices: ${indices}?` - -diff --git a/scripts/es/deleteIndex.js b/scripts/es/deleteIndex.js -index 6e30995..724d355 100644 ---- a/scripts/es/deleteIndex.js -+++ b/scripts/es/deleteIndex.js -@@ -8,7 +8,8 @@ const helper = require('../../src/common/helper') - const indices = [ - config.get('esConfig.ES_INDEX_JOB'), - config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), -- config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') -+ config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), -+ config.get('esConfig.ES_INDEX_ROLE') - ] - const userPrompt = `WARNING: this would remove existent data! Are you sure want to delete the following eleasticsearch indices: ${indices}?` - -diff --git a/scripts/es/reIndexAll.js b/scripts/es/reIndexAll.js -index 802695d..0367be1 100644 ---- a/scripts/es/reIndexAll.js -+++ b/scripts/es/reIndexAll.js -@@ -34,6 +34,7 @@ async function indexAll () { - await helper.indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) - await helper.indexBulkDataToES(jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger) - await helper.indexBulkDataToES(resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger) -+ await helper.indexBulkDataToES('Role', config.get('esConfig.ES_INDEX_ROLE'), logger) - process.exit(0) - } catch (err) { - logger.logFullError(err, { component: 'indexAll' }) -diff --git a/scripts/es/reIndexRoles.js b/scripts/es/reIndexRoles.js -new file mode 100644 -index 0000000..a4507aa ---- /dev/null -+++ b/scripts/es/reIndexRoles.js -@@ -0,0 +1,37 @@ -+/** -+ * Reindex Roles data in Elasticsearch using data from database -+ */ -+const config = require('config') -+const logger = require('../../src/common/logger') -+const helper = require('../../src/common/helper') -+ -+const roleId = helper.getParamFromCliArgs() -+const index = config.get('esConfig.ES_INDEX_ROLE') -+const reIndexAllRolesPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the index ${index}?` -+const reIndexRolePrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the document with id ${roleId} in index ${index}?` -+ -+async function reIndexRoles () { -+ if (roleId === null) { -+ await helper.promptUser(reIndexAllRolesPrompt, async () => { -+ try { -+ await helper.indexBulkDataToES('Role', index, logger) -+ process.exit(0) -+ } catch (err) { -+ logger.logFullError(err, { component: 'reIndexRoles' }) -+ process.exit(1) -+ } -+ }) -+ } else { -+ await helper.promptUser(reIndexRolePrompt, async () => { -+ try { -+ await helper.indexDataToEsById(roleId, 'Role', index, logger) -+ process.exit(0) -+ } catch (err) { -+ logger.logFullError(err, { component: 'reIndexRoles' }) -+ process.exit(1) -+ } -+ }) -+ } -+} -+ -+reIndexRoles() -diff --git a/src/bootstrap.js b/src/bootstrap.js -index 2999f13..896e6c9 100644 ---- a/src/bootstrap.js -+++ b/src/bootstrap.js -@@ -16,7 +16,7 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') - Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') - Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') - Joi.workload = () => Joi.string().valid('full-time', 'fractional') --Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed') -+Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed') - Joi.title = () => Joi.string().max(128) - Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') - Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) -@@ -26,6 +26,7 @@ Joi.workPeriodPaymentStatus = () => Joi.string().valid('completed', 'cancelled') - // See https://joi.dev/api/?v=17.3.0#string fro details why it's like this. - // In many cases we would like to allow empty string to make it easier to create UI for editing data. - Joi.stringAllowEmpty = () => Joi.string().allow('') -+Joi.smallint = () => Joi.number().min(-32768).max(32767) - - function buildServices (dir) { - const files = fs.readdirSync(dir) -diff --git a/src/common/helper.js b/src/common/helper.js -index 0ce1190..66cf32d 100644 ---- a/src/common/helper.js -+++ b/src/common/helper.js -@@ -2,50 +2,50 @@ - * This file defines helper methods - */ - --const fs = require('fs'); --const querystring = require('querystring'); --const Confirm = require('prompt-confirm'); --const Bottleneck = require('bottleneck'); --const AWS = require('aws-sdk'); --const config = require('config'); --const HttpStatus = require('http-status-codes'); --const _ = require('lodash'); --const request = require('superagent'); --const elasticsearch = require('@elastic/elasticsearch'); -+const fs = require('fs') -+const querystring = require('querystring') -+const Confirm = require('prompt-confirm') -+const Bottleneck = require('bottleneck') -+const AWS = require('aws-sdk') -+const config = require('config') -+const HttpStatus = require('http-status-codes') -+const _ = require('lodash') -+const request = require('superagent') -+const elasticsearch = require('@elastic/elasticsearch') - const { -- ResponseError: ESResponseError, --} = require('@elastic/elasticsearch/lib/errors'); --const errors = require('../common/errors'); --const logger = require('./logger'); --const models = require('../models'); --const eventDispatcher = require('./eventDispatcher'); --const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper'); --const moment = require('moment'); -+ ResponseError: ESResponseError -+} = require('@elastic/elasticsearch/lib/errors') -+const errors = require('../common/errors') -+const logger = require('./logger') -+const models = require('../models') -+const eventDispatcher = require('./eventDispatcher') -+const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') -+const moment = require('moment') - - const localLogger = { - debug: (message) => - logger.debug({ - component: 'helper', - context: message.context, -- message: message.message, -+ message: message.message - }), - error: (message) => - logger.error({ - component: 'helper', - context: message.context, -- message: message.message, -+ message: message.message - }), - info: (message) => - logger.info({ - component: 'helper', - context: message.context, -- message: message.message, -- }), --}; -+ message: message.message -+ }) -+} - --AWS.config.region = config.esConfig.AWS_REGION; -+AWS.config.region = config.esConfig.AWS_REGION - --const m2mAuth = require('tc-core-library-js').auth.m2m; -+const m2mAuth = require('tc-core-library-js').auth.m2m - - const m2m = m2mAuth( - _.pick(config, [ -@@ -53,9 +53,9 @@ const m2m = m2mAuth( - 'AUTH0_AUDIENCE', - 'AUTH0_CLIENT_ID', - 'AUTH0_CLIENT_SECRET', -- 'AUTH0_PROXY_SERVER_URL', -+ 'AUTH0_PROXY_SERVER_URL' - ]) --); -+) - - const m2mForUbahn = m2mAuth({ - AUTH0_AUDIENCE: config.AUTH0_AUDIENCE_UBAHN, -@@ -64,20 +64,20 @@ const m2mForUbahn = m2mAuth({ - 'TOKEN_CACHE_TIME', - 'AUTH0_CLIENT_ID', - 'AUTH0_CLIENT_SECRET', -- 'AUTH0_PROXY_SERVER_URL', -- ]), --}); -+ 'AUTH0_PROXY_SERVER_URL' -+ ]) -+}) - --let busApiClient; -+let busApiClient - - /** - * Get bus api client. - * - * @returns {Object} the bus api client - */ --function getBusApiClient() { -+function getBusApiClient () { - if (busApiClient) { -- return busApiClient; -+ return busApiClient - } - busApiClient = busApi( - _.pick(config, [ -@@ -88,17 +88,17 @@ function getBusApiClient() { - 'AUTH0_CLIENT_SECRET', - 'BUSAPI_URL', - 'KAFKA_ERROR_TOPIC', -- 'AUTH0_PROXY_SERVER_URL', -+ 'AUTH0_PROXY_SERVER_URL' - ]) -- ); -- return busApiClient; -+ ) -+ return busApiClient - } - - // ES Client mapping --const esClients = {}; -+const esClients = {} - - // The es index property mapping --const esIndexPropertyMapping = {}; -+const esIndexPropertyMapping = {} - esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { - projectId: { type: 'integer' }, - externalId: { type: 'keyword' }, -@@ -113,11 +113,12 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { - skills: { type: 'keyword' }, - status: { type: 'keyword' }, - isApplicationPageActive: { type: 'boolean' }, -+ roleIds: { type: 'keyword' }, - createdAt: { type: 'date' }, - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, -- updatedBy: { type: 'keyword' }, --}; -+ updatedBy: { type: 'keyword' } -+} - esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { - jobId: { type: 'keyword' }, - userId: { type: 'keyword' }, -@@ -150,14 +151,14 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -- deletedAt: { type: 'date' }, -- }, -+ deletedAt: { type: 'date' } -+ } - }, - createdAt: { type: 'date' }, - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, -- updatedBy: { type: 'keyword' }, --}; -+ updatedBy: { type: 'keyword' } -+} - esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { - projectId: { type: 'integer' }, - userId: { type: 'keyword' }, -@@ -195,32 +196,59 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { - createdAt: { type: 'date' }, - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, -- updatedBy: { type: 'keyword' }, -- }, -+ updatedBy: { type: 'keyword' } -+ } - }, - createdAt: { type: 'date' }, - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, -- updatedBy: { type: 'keyword' }, -- }, -+ updatedBy: { type: 'keyword' } -+ } -+ }, -+ createdAt: { type: 'date' }, -+ createdBy: { type: 'keyword' }, -+ updatedAt: { type: 'date' }, -+ updatedBy: { type: 'keyword' } -+} -+esIndexPropertyMapping[config.get('esConfig.ES_INDEX_ROLE')] = { -+ name: { type: 'keyword' }, -+ description: { type: 'keyword' }, -+ listOfSkills: { type: 'keyword' }, -+ rates: { -+ properties: { -+ global: { type: 'integer' }, -+ inCountry: { type: 'integer' }, -+ offShore: { type: 'integer' }, -+ rate30Global: { type: 'integer' }, -+ rate30InCountry: { type: 'integer' }, -+ rate30OffShore: { type: 'integer' }, -+ rate20Global: { type: 'integer' }, -+ rate20InCountry: { type: 'integer' }, -+ rate20OffShore: { type: 'integer' } -+ } - }, -+ numberOfMembers: { type: 'integer' }, -+ numberOfMembersAvailable: { type: 'integer' }, -+ imageUrl: { type: 'keyword' }, -+ timeToCandidate: { type: 'integer' }, -+ timeToInterview: { type: 'integer' }, - createdAt: { type: 'date' }, - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, -- updatedBy: { type: 'keyword' }, --}; -+ updatedBy: { type: 'keyword' } -+} - - /** - * Get the first parameter from cli arguments - */ --function getParamFromCliArgs() { -- const filteredArgs = process.argv.filter((arg) => !arg.includes('--')); -+function getParamFromCliArgs () { -+ const filteredArgs = process.argv.filter((arg) => !arg.includes('--')) - - if (filteredArgs.length > 2) { -- return filteredArgs[2]; -+ return filteredArgs[2] - } - -- return null; -+ return null - } - - /** -@@ -228,18 +256,18 @@ function getParamFromCliArgs() { - * @param {string} promptQuery the query to ask the user - * @param {function} cb the callback function - */ --async function promptUser(promptQuery, cb) { -+async function promptUser (promptQuery, cb) { - if (process.argv.includes('--force')) { -- await cb(); -- return; -+ await cb() -+ return - } - -- const prompt = new Confirm(promptQuery); -+ const prompt = new Confirm(promptQuery) - prompt.ask(async (answer) => { - if (answer) { -- await cb(); -+ await cb() - } -- }); -+ }) - } - - /** -@@ -248,23 +276,23 @@ async function promptUser(promptQuery, cb) { - * @param {Object} logger the logger object - * @param {Object} esClient the elasticsearch client (optional, will create if not given) - */ --async function createIndex(index, logger, esClient = null) { -+async function createIndex (index, logger, esClient = null) { - if (!esClient) { -- esClient = getESClient(); -+ esClient = getESClient() - } - - await esClient.indices.create({ - index, - body: { - mappings: { -- properties: esIndexPropertyMapping[index], -- }, -- }, -- }); -+ properties: esIndexPropertyMapping[index] -+ } -+ } -+ }) - logger.info({ - component: 'createIndex', -- message: `ES Index ${index} creation succeeded!`, -- }); -+ message: `ES Index ${index} creation succeeded!` -+ }) - } - - /** -@@ -273,45 +301,45 @@ async function createIndex(index, logger, esClient = null) { - * @param {Object} logger the logger object - * @param {Object} esClient the elasticsearch client (optional, will create if not given) - */ --async function deleteIndex(index, logger, esClient = null) { -+async function deleteIndex (index, logger, esClient = null) { - if (!esClient) { -- esClient = getESClient(); -+ esClient = getESClient() - } - -- await esClient.indices.delete({ index }); -+ await esClient.indices.delete({ index }) - logger.info({ - component: 'deleteIndex', -- message: `ES Index ${index} deletion succeeded!`, -- }); -+ message: `ES Index ${index} deletion succeeded!` -+ }) - } - - /** - * Split data into bulks - * @param {Array} data the array of data to split - */ --function getBulksFromDocuments(data) { -- const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6; -- const bulks = []; -- let documentIndex = 0; -- let currentBulkSize = 0; -- let currentBulk = []; -+function getBulksFromDocuments (data) { -+ const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6 -+ const bulks = [] -+ let documentIndex = 0 -+ let currentBulkSize = 0 -+ let currentBulk = [] - - while (true) { - // break loop when parsed all documents - if (documentIndex >= data.length) { -- bulks.push(currentBulk); -- break; -+ bulks.push(currentBulk) -+ break - } - - // check if current document size is greater than the max bulk size, if so, throw error - const currentDocumentSize = Buffer.byteLength( - JSON.stringify(data[documentIndex]), - 'utf-8' -- ); -+ ) - if (maxBytes < currentDocumentSize) { - throw new Error( - `Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.` -- ); -+ ) - } - - if ( -@@ -320,17 +348,17 @@ function getBulksFromDocuments(data) { - ) { - // if adding the current document goes over the max bulk size OR goes over max number of docs - // then push the current bulk to bulks array and reset the current bulk -- bulks.push(currentBulk); -- currentBulk = []; -- currentBulkSize = 0; -+ bulks.push(currentBulk) -+ currentBulk = [] -+ currentBulkSize = 0 - } else { - // otherwise, add document to current bulk -- currentBulk.push(data[documentIndex]); -- currentBulkSize += currentDocumentSize; -- documentIndex++; -+ currentBulk.push(data[documentIndex]) -+ currentBulkSize += currentDocumentSize -+ documentIndex++ - } - } -- return bulks; -+ return bulks - } - - /** -@@ -339,57 +367,57 @@ function getBulksFromDocuments(data) { - * @param {Object} indexName the index name - * @param {Object} logger the logger object - */ --async function indexBulkDataToES(modelOpts, indexName, logger) { -- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; -- const include = _.get(modelOpts, 'include', []); -+async function indexBulkDataToES (modelOpts, indexName, logger) { -+ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName -+ const include = _.get(modelOpts, 'include', []) - - logger.info({ - component: 'indexBulkDataToES', -- message: `Reindexing of ${modelName}s started!`, -- }); -+ message: `Reindexing of ${modelName}s started!` -+ }) - -- const esClient = getESClient(); -+ const esClient = getESClient() - - // clear index -- const indexExistsRes = await esClient.indices.exists({ index: indexName }); -+ const indexExistsRes = await esClient.indices.exists({ index: indexName }) - if (indexExistsRes.statusCode !== 404) { -- await deleteIndex(indexName, logger, esClient); -+ await deleteIndex(indexName, logger, esClient) - } -- await createIndex(indexName, logger, esClient); -+ await createIndex(indexName, logger, esClient) - - // get data from db - logger.info({ - component: 'indexBulkDataToES', -- message: 'Getting data from database', -- }); -- const model = models[modelName]; -- const data = await model.findAll({ include }); -- const rawObjects = _.map(data, (r) => r.toJSON()); -+ message: 'Getting data from database' -+ }) -+ const model = models[modelName] -+ const data = await model.findAll({ include }) -+ const rawObjects = _.map(data, (r) => r.toJSON()) - if (_.isEmpty(rawObjects)) { - logger.info({ - component: 'indexBulkDataToES', -- message: `No data in database for ${modelName}`, -- }); -- return; -+ message: `No data in database for ${modelName}` -+ }) -+ return - } -- const bulks = getBulksFromDocuments(rawObjects); -+ const bulks = getBulksFromDocuments(rawObjects) - -- const startTime = Date.now(); -- let doneCount = 0; -+ const startTime = Date.now() -+ let doneCount = 0 - for (const bulk of bulks) { - // send bulk to esclient - const body = bulk.flatMap((doc) => [ - { index: { _index: indexName, _id: doc.id } }, -- doc, -- ]); -- await esClient.bulk({ refresh: true, body }); -- doneCount += bulk.length; -+ doc -+ ]) -+ await esClient.bulk({ refresh: true, body }) -+ doneCount += bulk.length - - // log metrics -- const timeSpent = Date.now() - startTime; -- const avgTimePerDocument = timeSpent / doneCount; -- const estimatedLength = avgTimePerDocument * data.length; -- const timeLeft = startTime + estimatedLength - Date.now(); -+ const timeSpent = Date.now() - startTime -+ const avgTimePerDocument = timeSpent / doneCount -+ const estimatedLength = avgTimePerDocument * data.length -+ const timeLeft = startTime + estimatedLength - Date.now() - logger.info({ - component: 'indexBulkDataToES', - message: `Processed ${doneCount} of ${ -@@ -398,8 +426,8 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { - avgTimePerDocument - )}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime( - timeLeft -- )}`, -- }); -+ )}` -+ }) - } - } - -@@ -410,36 +438,36 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { - * @param {string} id the job id - * @param {Object} logger the logger object - */ --async function indexDataToEsById(id, modelOpts, indexName, logger) { -- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; -- const include = _.get(modelOpts, 'include', []); -+async function indexDataToEsById (id, modelOpts, indexName, logger) { -+ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName -+ const include = _.get(modelOpts, 'include', []) - - logger.info({ - component: 'indexDataToEsById', -- message: `Reindexing of ${modelName} with id ${id} started!`, -- }); -- const esClient = getESClient(); -+ message: `Reindexing of ${modelName} with id ${id} started!` -+ }) -+ const esClient = getESClient() - - logger.info({ - component: 'indexDataToEsById', -- message: 'Getting data from database', -- }); -- const model = models[modelName]; -+ message: 'Getting data from database' -+ }) -+ const model = models[modelName] - -- const data = await model.findById(id, include); -+ const data = await model.findById(id, include) - logger.info({ - component: 'indexDataToEsById', -- message: 'Indexing data into Elasticsearch', -- }); -+ message: 'Indexing data into Elasticsearch' -+ }) - await esClient.index({ - index: indexName, - id: id, -- body: data.dataValues, -- }); -+ body: data.dataValues -+ }) - logger.info({ - component: 'indexDataToEsById', -- message: 'Indexing complete!', -- }); -+ message: 'Indexing complete!' -+ }) - } - - /** -@@ -448,68 +476,68 @@ async function indexDataToEsById(id, modelOpts, indexName, logger) { - * @param {Array} dataModels the data models to import - * @param {Object} logger the logger object - */ --async function importData(pathToFile, dataModels, logger) { -+async function importData (pathToFile, dataModels, logger) { - // check if file exists - if (!fs.existsSync(pathToFile)) { -- throw new Error(`File with path ${pathToFile} does not exist`); -+ throw new Error(`File with path ${pathToFile} does not exist`) - } - - // clear database -- logger.info({ component: 'importData', message: 'Clearing database...' }); -- await models.sequelize.sync({ force: true }); -+ logger.info({ component: 'importData', message: 'Clearing database...' }) -+ await models.sequelize.sync({ force: true }) - -- let transaction = null; -- let currentModelName = null; -+ let transaction = null -+ let currentModelName = null - try { - // Start a transaction -- transaction = await models.sequelize.transaction(); -- const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()); -+ transaction = await models.sequelize.transaction() -+ const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()) - - for (let index = 0; index < dataModels.length; index += 1) { -- const modelOpts = dataModels[index]; -- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; -- const include = _.get(modelOpts, 'include', []); -+ const modelOpts = dataModels[index] -+ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName -+ const include = _.get(modelOpts, 'include', []) - -- currentModelName = modelName; -- const model = models[modelName]; -- const modelRecords = jsonData[modelName]; -+ currentModelName = modelName -+ const model = models[modelName] -+ const modelRecords = jsonData[modelName] - - if (modelRecords && modelRecords.length > 0) { - logger.info({ - component: 'importData', -- message: `Importing data for model: ${modelName}`, -- }); -+ message: `Importing data for model: ${modelName}` -+ }) - -- await model.bulkCreate(modelRecords, { include, transaction }); -+ await model.bulkCreate(modelRecords, { include, transaction }) - logger.info({ - component: 'importData', -- message: `Records imported for model: ${modelName} = ${modelRecords.length}`, -- }); -+ message: `Records imported for model: ${modelName} = ${modelRecords.length}` -+ }) - } else { - logger.info({ - component: 'importData', -- message: `No records to import for model: ${modelName}`, -- }); -+ message: `No records to import for model: ${modelName}` -+ }) - } - } - // commit transaction only if all things went ok - logger.info({ - component: 'importData', -- message: 'committing transaction to database...', -- }); -- await transaction.commit(); -+ message: 'committing transaction to database...' -+ }) -+ await transaction.commit() - } catch (error) { - logger.error({ - component: 'importData', -- message: `Error while writing data of model: ${currentModelName}`, -- }); -+ message: `Error while writing data of model: ${currentModelName}` -+ }) - // rollback all insert operations - if (transaction) { - logger.info({ - component: 'importData', -- message: 'rollback database transaction...', -- }); -- transaction.rollback(); -+ message: 'rollback database transaction...' -+ }) -+ transaction.rollback() - } - if (error.name && error.errors && error.fields) { - // For sequelize validation errors, we throw only fields with data that helps in debugging error, -@@ -519,11 +547,11 @@ async function importData(pathToFile, dataModels, logger) { - modelName: currentModelName, - name: error.name, - errors: error.errors, -- fields: error.fields, -+ fields: error.fields - }) -- ); -+ ) - } else { -- throw error; -+ throw error - } - } - -@@ -533,10 +561,10 @@ async function importData(pathToFile, dataModels, logger) { - include: [ - { - model: models.Interview, -- as: 'interviews', -- }, -- ], -- }; -+ as: 'interviews' -+ } -+ ] -+ } - const resourceBookingModelOpts = { - modelName: 'ResourceBooking', - include: [ -@@ -546,23 +574,24 @@ async function importData(pathToFile, dataModels, logger) { - include: [ - { - model: models.WorkPeriodPayment, -- as: 'payments', -- }, -- ], -- }, -- ], -- }; -- await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger); -+ as: 'payments' -+ } -+ ] -+ } -+ ] -+ } -+ await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) - await indexBulkDataToES( - jobCandidateModelOpts, - config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - logger -- ); -+ ) - await indexBulkDataToES( - resourceBookingModelOpts, - config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), - logger -- ); -+ ) -+ await indexBulkDataToES('Role', config.get('esConfig.ES_INDEX_ROLE'), logger) - } - - /** -@@ -571,74 +600,74 @@ async function importData(pathToFile, dataModels, logger) { - * @param {Array} dataModels the data models to export - * @param {Object} logger the logger object - */ --async function exportData(pathToFile, dataModels, logger) { -+async function exportData (pathToFile, dataModels, logger) { - logger.info({ - component: 'exportData', -- message: `Start Saving data to file with path ${pathToFile}....`, -- }); -+ message: `Start Saving data to file with path ${pathToFile}....` -+ }) - -- const allModelsRecords = {}; -+ const allModelsRecords = {} - for (let index = 0; index < dataModels.length; index += 1) { -- const modelOpts = dataModels[index]; -- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; -- const include = _.get(modelOpts, 'include', []); -- const modelRecords = await models[modelName].findAll({ include }); -- const rawRecords = _.map(modelRecords, (r) => r.toJSON()); -- allModelsRecords[modelName] = rawRecords; -+ const modelOpts = dataModels[index] -+ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName -+ const include = _.get(modelOpts, 'include', []) -+ const modelRecords = await models[modelName].findAll({ include }) -+ const rawRecords = _.map(modelRecords, (r) => r.toJSON()) -+ allModelsRecords[modelName] = rawRecords - logger.info({ - component: 'exportData', -- message: `Records loaded for model: ${modelName} = ${rawRecords.length}`, -- }); -+ message: `Records loaded for model: ${modelName} = ${rawRecords.length}` -+ }) - } - -- fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)); -+ fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)) - logger.info({ - component: 'exportData', -- message: 'End Saving data to file....', -- }); -+ message: 'End Saving data to file....' -+ }) - } - - /** - * Format a time in milliseconds into a human readable format - * @param {Date} milliseconds the number of milliseconds - */ --function formatTime(millisec) { -- const ms = Math.floor(millisec % 1000); -- const secs = Math.floor((millisec / 1000) % 60); -- const mins = Math.floor((millisec / (1000 * 60)) % 60); -- const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24); -- const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7); -- const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4); -- const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12); -- const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)); -- -- let formattedTime = '0 milliseconds'; -+function formatTime (millisec) { -+ const ms = Math.floor(millisec % 1000) -+ const secs = Math.floor((millisec / 1000) % 60) -+ const mins = Math.floor((millisec / (1000 * 60)) % 60) -+ const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24) -+ const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7) -+ const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4) -+ const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12) -+ const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)) -+ -+ let formattedTime = '0 milliseconds' - if (ms > 0) { -- formattedTime = `${ms} milliseconds`; -+ formattedTime = `${ms} milliseconds` - } - if (secs > 0) { -- formattedTime = `${secs} seconds ${formattedTime}`; -+ formattedTime = `${secs} seconds ${formattedTime}` - } - if (mins > 0) { -- formattedTime = `${mins} minutes ${formattedTime}`; -+ formattedTime = `${mins} minutes ${formattedTime}` - } - if (hrs > 0) { -- formattedTime = `${hrs} hours ${formattedTime}`; -+ formattedTime = `${hrs} hours ${formattedTime}` - } - if (days > 0) { -- formattedTime = `${days} days ${formattedTime}`; -+ formattedTime = `${days} days ${formattedTime}` - } - if (weeks > 0) { -- formattedTime = `${weeks} weeks ${formattedTime}`; -+ formattedTime = `${weeks} weeks ${formattedTime}` - } - if (mnths > 0) { -- formattedTime = `${mnths} months ${formattedTime}`; -+ formattedTime = `${mnths} months ${formattedTime}` - } - if (yrs > 0) { -- formattedTime = `${yrs} years ${formattedTime}`; -+ formattedTime = `${yrs} years ${formattedTime}` - } - -- return formattedTime.trim(); -+ return formattedTime.trim() - } - - /** -@@ -647,30 +676,30 @@ function formatTime(millisec) { - * @param {Array} source the array in which to search for the term - * @param {Array | String} term the term to search - */ --function checkIfExists(source, term) { -- let terms; -+function checkIfExists (source, term) { -+ let terms - - if (!_.isArray(source)) { -- throw new Error('Source argument should be an array'); -+ throw new Error('Source argument should be an array') - } - -- source = source.map((s) => s.toLowerCase()); -+ source = source.map((s) => s.toLowerCase()) - - if (_.isString(term)) { -- terms = term.toLowerCase().split(' '); -+ terms = term.toLowerCase().split(' ') - } else if (_.isArray(term)) { -- terms = term.map((t) => t.toLowerCase()); -+ terms = term.map((t) => t.toLowerCase()) - } else { -- throw new Error('Term argument should be either a string or an array'); -+ throw new Error('Term argument should be either a string or an array') - } - - for (let i = 0; i < terms.length; i++) { - if (source.includes(terms[i])) { -- return true; -+ return true - } - } - -- return false; -+ return false - } - - /** -@@ -678,10 +707,10 @@ function checkIfExists(source, term) { - * @param {Function} fn the async function - * @returns {Function} the wrapped function - */ --function wrapExpress(fn) { -+function wrapExpress (fn) { - return function (req, res, next) { -- fn(req, res, next).catch(next); -- }; -+ fn(req, res, next).catch(next) -+ } - } - - /** -@@ -689,20 +718,20 @@ function wrapExpress(fn) { - * @param obj the object (controller exports) - * @returns {Object|Array} the wrapped object - */ --function autoWrapExpress(obj) { -+function autoWrapExpress (obj) { - if (_.isArray(obj)) { -- return obj.map(autoWrapExpress); -+ return obj.map(autoWrapExpress) - } - if (_.isFunction(obj)) { - if (obj.constructor.name === 'AsyncFunction') { -- return wrapExpress(obj); -+ return wrapExpress(obj) - } -- return obj; -+ return obj - } - _.each(obj, (value, key) => { -- obj[key] = autoWrapExpress(value); -- }); -- return obj; -+ obj[key] = autoWrapExpress(value) -+ }) -+ return obj - } - - /** -@@ -711,11 +740,11 @@ function autoWrapExpress(obj) { - * @param {Number} page the page number - * @returns {String} link for the page - */ --function getPageLink(req, page) { -- const q = _.assignIn({}, req.query, { page }); -+function getPageLink (req, page) { -+ const q = _.assignIn({}, req.query, { page }) - return `${req.protocol}://${req.get('Host')}${req.baseUrl}${ - req.path -- }?${querystring.stringify(q)}`; -+ }?${querystring.stringify(q)}` - } - - /** -@@ -724,31 +753,31 @@ function getPageLink(req, page) { - * @param {Object} res the HTTP response - * @param {Object} result the operation result - */ --function setResHeaders(req, res, result) { -- const totalPages = Math.ceil(result.total / result.perPage); -+function setResHeaders (req, res, result) { -+ const totalPages = Math.ceil(result.total / result.perPage) - if (result.page > 1) { -- res.set('X-Prev-Page', result.page - 1); -+ res.set('X-Prev-Page', result.page - 1) - } - if (result.page < totalPages) { -- res.set('X-Next-Page', result.page + 1); -+ res.set('X-Next-Page', result.page + 1) - } -- res.set('X-Page', result.page); -- res.set('X-Per-Page', result.perPage); -- res.set('X-Total', result.total); -- res.set('X-Total-Pages', totalPages); -+ res.set('X-Page', result.page) -+ res.set('X-Per-Page', result.perPage) -+ res.set('X-Total', result.total) -+ res.set('X-Total-Pages', totalPages) - // set Link header - if (totalPages > 0) { - let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink( - req, - totalPages -- )}>; rel="last"`; -+ )}>; rel="last"` - if (result.page > 1) { -- link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"`; -+ link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"` - } - if (result.page < totalPages) { -- link += `, <${getPageLink(req, result.page + 1)}>; rel="next"`; -+ link += `, <${getPageLink(req, result.page + 1)}>; rel="next"` - } -- res.set('Link', link); -+ res.set('Link', link) - } - } - -@@ -756,30 +785,30 @@ function setResHeaders(req, res, result) { - * Get ES Client - * @return {Object} Elastic Host Client Instance - */ --function getESClient() { -+function getESClient () { - if (esClients.client) { -- return esClients.client; -+ return esClients.client - } - -- const host = config.esConfig.HOST; -- const cloudId = config.esConfig.ELASTICCLOUD.id; -+ const host = config.esConfig.HOST -+ const cloudId = config.esConfig.ELASTICCLOUD.id - if (cloudId) { - // Elastic Cloud configuration - esClients.client = new elasticsearch.Client({ - cloud: { -- id: cloudId, -+ id: cloudId - }, - auth: { - username: config.esConfig.ELASTICCLOUD.username, -- password: config.esConfig.ELASTICCLOUD.password, -- }, -- }); -+ password: config.esConfig.ELASTICCLOUD.password -+ } -+ }) - } else { - esClients.client = new elasticsearch.Client({ -- node: host, -- }); -+ node: host -+ }) - } -- return esClients.client; -+ return esClients.client - } - - /* -@@ -790,8 +819,8 @@ const getM2MToken = async () => { - return await m2m.getMachineToken( - config.AUTH0_CLIENT_ID, - config.AUTH0_CLIENT_SECRET -- ); --}; -+ ) -+} - - /* - * Function to get M2M token for U-Bahn -@@ -801,8 +830,8 @@ const getM2MUbahnToken = async () => { - return await m2mForUbahn.getMachineToken( - config.AUTH0_CLIENT_ID, - config.AUTH0_CLIENT_SECRET -- ); --}; -+ ) -+} - - /** - * Function to encode query string -@@ -810,17 +839,17 @@ const getM2MUbahnToken = async () => { - * @param {String} nesting the nesting string - * @returns {String} query string - */ --function encodeQueryString(queryObj, nesting = '') { -+function encodeQueryString (queryObj, nesting = '') { - const pairs = Object.entries(queryObj).map(([key, val]) => { - // Handle the nested, recursive case, where the value to encode is an object itself - if (typeof val === 'object') { -- return encodeQueryString(val, nesting + `${key}.`); -+ return encodeQueryString(val, nesting + `${key}.`) - } else { - // Handle base case, where the value to encode is simply a string. -- return [nesting + key, val].map(querystring.escape).join('='); -+ return [nesting + key, val].map(querystring.escape).join('=') - } -- }); -- return pairs.join('&'); -+ }) -+ return pairs.join('&') - } - - /** -@@ -828,31 +857,31 @@ function encodeQueryString(queryObj, nesting = '') { - * @param {Integer} externalId the legacy user id - * @returns {Array} the users found - */ --async function listUsersByExternalId(externalId) { -+async function listUsersByExternalId (externalId) { - // return empty list if externalId is null or undefined - if (!!externalId !== true) { -- return []; -+ return [] - } - -- const token = await getM2MUbahnToken(); -+ const token = await getM2MUbahnToken() - const q = { - enrich: true, - externalProfile: { - organizationId: config.ORG_ID, -- externalId, -- }, -- }; -- const url = `${config.TC_API}/users?${encodeQueryString(q)}`; -+ externalId -+ } -+ } -+ const url = `${config.TC_API}/users?${encodeQueryString(q)}` - const res = await request - .get(url) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'listUserByExternalId', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return res.body; -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return res.body - } - - /** -@@ -860,14 +889,14 @@ async function listUsersByExternalId(externalId) { - * @param {Integer} externalId the legacy user id - * @returns {Object} the user - */ --async function getUserByExternalId(externalId) { -- const users = await listUsersByExternalId(externalId); -+async function getUserByExternalId (externalId) { -+ const users = await listUsersByExternalId(externalId) - if (_.isEmpty(users)) { - throw new errors.NotFoundError( - `externalId: ${externalId} "user" not found` -- ); -+ ) - } -- return users[0]; -+ return users[0] - } - - /** -@@ -876,24 +905,24 @@ async function getUserByExternalId(externalId) { - * @params {Object} payload the payload - * @params {Object} options the extra options to control the function - */ --async function postEvent(topic, payload, options = {}) { -+async function postEvent (topic, payload, options = {}) { - logger.debug({ - component: 'helper', - context: 'postEvent', - message: `Posting event to Kafka topic ${topic}, ${JSON.stringify( - payload -- )}`, -- }); -- const client = getBusApiClient(); -+ )}` -+ }) -+ const client = getBusApiClient() - const message = { - topic, - originator: config.KAFKA_MESSAGE_ORIGINATOR, - timestamp: new Date().toISOString(), - 'mime-type': 'application/json', -- payload, -- }; -- await client.postEvent(message); -- await eventDispatcher.handleEvent(topic, { value: payload, options }); -+ payload -+ } -+ await client.postEvent(message) -+ await eventDispatcher.handleEvent(topic, { value: payload, options }) - } - - /** -@@ -902,11 +931,11 @@ async function postEvent(topic, payload, options = {}) { - * @param {Object} err the err - * @returns {Boolean} the result - */ --function isDocumentMissingException(err) { -+function isDocumentMissingException (err) { - if (err.statusCode === 404 && err instanceof ESResponseError) { -- return true; -+ return true - } -- return false; -+ return false - } - - /** -@@ -915,34 +944,34 @@ function isDocumentMissingException(err) { - * @param {Object} criteria the search criteria - * @returns the request result - */ --async function getProjects(currentUser, criteria = {}) { -- let token; -+async function getProjects (currentUser, criteria = {}) { -+ let token - if (currentUser.hasManagePermission || currentUser.isMachine) { -- const m2mToken = await getM2MToken(); -- token = `Bearer ${m2mToken}`; -+ const m2mToken = await getM2MToken() -+ token = `Bearer ${m2mToken}` - } else { -- token = currentUser.jwtToken; -+ token = currentUser.jwtToken - } -- const url = `${config.TC_API}/projects?type=talent-as-a-service`; -+ const url = `${config.TC_API}/projects?type=talent-as-a-service` - const res = await request - .get(url) - .query(criteria) - .set('Authorization', token) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getProjects', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) - const result = _.map(res.body, (item) => { -- return _.pick(item, ['id', 'name', 'invites', 'members']); -- }); -+ return _.pick(item, ['id', 'name', 'invites', 'members']) -+ }) - return { - total: Number(_.get(res.headers, 'x-total')), - page: Number(_.get(res.headers, 'x-page')), - perPage: Number(_.get(res.headers, 'x-per-page')), -- result, -- }; -+ result -+ } - } - - /** -@@ -951,24 +980,24 @@ async function getProjects(currentUser, criteria = {}) { - * @param {String} userId the legacy user id - * @returns {Object} the user - */ --async function getTopcoderUserById(userId) { -- const token = await getM2MToken(); -+async function getTopcoderUserById (userId) { -+ const token = await getM2MToken() - const res = await request - .get(config.TOPCODER_USERS_API) - .query({ filter: `id=${userId}` }) - .set('Authorization', `Bearer ${token}`) -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getTopcoderUserById', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- const user = _.get(res.body, 'result.content[0]'); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ const user = _.get(res.body, 'result.content[0]') - if (!user) { - throw new errors.NotFoundError( - `userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}` -- ); -+ ) - } -- return user; -+ return user - } - - /** -@@ -976,31 +1005,31 @@ async function getTopcoderUserById(userId) { - * @param {String} userId the user id - * @returns the request result - */ --async function getUserById(userId, enrich) { -- const token = await getM2MUbahnToken(); -+async function getUserById (userId, enrich) { -+ const token = await getM2MUbahnToken() - const res = await request - .get(`${config.TC_API}/users/${userId}` + (enrich ? '?enrich=true' : '')) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getUserById', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) - -- const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']); -+ const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']) - - if (enrich) { - user.skills = (res.body.skills || []).map((skillObj) => - _.pick(skillObj.skill, ['id', 'name']) -- ); -- const attributes = _.get(res, 'body.attributes', []); -+ ) -+ const attributes = _.get(res, 'body.attributes', []) - user.attributes = _.map(attributes, (attr) => - _.pick(attr, ['id', 'value', 'attribute.id', 'attribute.name']) -- ); -+ ) - } - -- return user; -+ return user - } - - /** -@@ -1008,19 +1037,19 @@ async function getUserById(userId, enrich) { - * @param {Object} data the user data - * @returns the request result - */ --async function createUbahnUser({ handle, firstName, lastName }) { -- const token = await getM2MUbahnToken(); -+async function createUbahnUser ({ handle, firstName, lastName }) { -+ const token = await getM2MUbahnToken() - const res = await request - .post(`${config.TC_API}/users`) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') -- .send({ handle, firstName, lastName }); -+ .send({ handle, firstName, lastName }) - localLogger.debug({ - context: 'createUbahnUser', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return _.pick(res.body, ['id']); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return _.pick(res.body, ['id']) - } - - /** -@@ -1028,21 +1057,21 @@ async function createUbahnUser({ handle, firstName, lastName }) { - * @param {String} userId the user id(with uuid format) - * @param {Object} data the profile data - */ --async function createUserExternalProfile( -+async function createUserExternalProfile ( - userId, - { organizationId, externalId } - ) { -- const token = await getM2MUbahnToken(); -+ const token = await getM2MUbahnToken() - const res = await request - .post(`${config.TC_API}/users/${userId}/externalProfiles`) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') -- .send({ organizationId, externalId: String(externalId) }); -+ .send({ organizationId, externalId: String(externalId) }) - localLogger.debug({ - context: 'createUserExternalProfile', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) - } - - /** -@@ -1050,23 +1079,23 @@ async function createUserExternalProfile( - * @param {Array} handles the handle array - * @returns the request result - */ --async function getMembers(handles) { -- const token = await getM2MToken(); -+async function getMembers (handles) { -+ const token = await getM2MToken() - const handlesStr = _.map(handles, (handle) => { -- return '%22' + handle.toLowerCase() + '%22'; -- }).join(','); -- const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]`; -+ return '%22' + handle.toLowerCase() + '%22' -+ }).join(',') -+ const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]` - - const res = await request - .get(url) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getMembers', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return res.body; -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return res.body - } - - /** -@@ -1075,36 +1104,36 @@ async function getMembers(handles) { - * @param {Number} id project id - * @returns the request result - */ --async function getProjectById(currentUser, id) { -- let token; -+async function getProjectById (currentUser, id) { -+ let token - if (currentUser.hasManagePermission || currentUser.isMachine) { -- const m2mToken = await getM2MToken(); -- token = `Bearer ${m2mToken}`; -+ const m2mToken = await getM2MToken() -+ token = `Bearer ${m2mToken}` - } else { -- token = currentUser.jwtToken; -+ token = currentUser.jwtToken - } -- const url = `${config.TC_API}/projects/${id}`; -+ const url = `${config.TC_API}/projects/${id}` - try { - const res = await request - .get(url) - .set('Authorization', token) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getProjectById', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return _.pick(res.body, ['id', 'name', 'invites', 'members']); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return _.pick(res.body, ['id', 'name', 'invites', 'members']) - } catch (err) { - if (err.status === HttpStatus.FORBIDDEN) { - throw new errors.ForbiddenError( - `You are not allowed to access the project with id ${id}` -- ); -+ ) - } - if (err.status === HttpStatus.NOT_FOUND) { -- throw new errors.NotFoundError(`id: ${id} project not found`); -+ throw new errors.NotFoundError(`id: ${id} project not found`) - } -- throw err; -+ throw err - } - } - -@@ -1115,33 +1144,33 @@ async function getProjectById(currentUser, id) { - * @param {Object} criteria the search criteria - * @returns the request result - */ --async function getTopcoderSkills(criteria) { -- const token = await getM2MUbahnToken(); -+async function getTopcoderSkills (criteria) { -+ const token = await getM2MUbahnToken() - try { - const res = await request - .get(`${config.TC_API}/skills`) - .query({ - skillProviderId: config.TOPCODER_SKILL_PROVIDER_ID, -- ...criteria, -+ ...criteria - }) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getTopcoderSkills', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) - return { - total: Number(_.get(res.headers, 'x-total')), - page: Number(_.get(res.headers, 'x-page')), - perPage: Number(_.get(res.headers, 'x-per-page')), -- result: res.body, -- }; -+ result: res.body -+ } - } catch (err) { - if (err.status === HttpStatus.BAD_REQUEST) { -- throw new errors.BadRequestError(err.response.body.message); -+ throw new errors.BadRequestError(err.response.body.message) - } -- throw err; -+ throw err - } - } - -@@ -1150,18 +1179,18 @@ async function getTopcoderSkills(criteria) { - * @param {String} skillId the skill Id - * @returns the request result - */ --async function getSkillById(skillId) { -- const token = await getM2MUbahnToken(); -+async function getSkillById (skillId) { -+ const token = await getM2MUbahnToken() - const res = await request - .get(`${config.TC_API}/skills/${skillId}`) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getSkillById', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return _.pick(res.body, ['id', 'name']); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return _.pick(res.body, ['id', 'name']) - } - - /** -@@ -1174,22 +1203,22 @@ async function getSkillById(skillId) { - * @params {Object} currentUser the user who perform this operation - * @returns {String} the ubahn user id - */ --async function ensureUbahnUserId(currentUser) { -+async function ensureUbahnUserId (currentUser) { - try { -- return (await getUserByExternalId(currentUser.userId)).id; -+ return (await getUserByExternalId(currentUser.userId)).id - } catch (err) { - if (!(err instanceof errors.NotFoundError)) { -- throw err; -+ throw err - } -- const topcoderUser = await getTopcoderUserById(currentUser.userId); -+ const topcoderUser = await getTopcoderUserById(currentUser.userId) - const user = await createUbahnUser( - _.pick(topcoderUser, ['handle', 'firstName', 'lastName']) -- ); -+ ) - await createUserExternalProfile(user.id, { - organizationId: config.ORG_ID, -- externalId: currentUser.userId, -- }); -- return user.id; -+ externalId: currentUser.userId -+ }) -+ return user.id - } - } - -@@ -1199,8 +1228,8 @@ async function ensureUbahnUserId(currentUser) { - * @param {String} jobId the job id - * @returns {Object} the job data - */ --async function ensureJobById(jobId) { -- return models.Job.findById(jobId); -+async function ensureJobById (jobId) { -+ return models.Job.findById(jobId) - } - - /** -@@ -1209,8 +1238,8 @@ async function ensureJobById(jobId) { - * @param {String} resourceBookingId the resourceBooking id - * @returns {Object} the resourceBooking data - */ --async function ensureResourceBookingById(resourceBookingId) { -- return models.ResourceBooking.findById(resourceBookingId); -+async function ensureResourceBookingById (resourceBookingId) { -+ return models.ResourceBooking.findById(resourceBookingId) - } - - /** -@@ -1218,8 +1247,8 @@ async function ensureResourceBookingById(resourceBookingId) { - * @param {String} workPeriodId the workPeriod id - * @returns the workPeriod data - */ --async function ensureWorkPeriodById(workPeriodId) { -- return models.WorkPeriod.findById(workPeriodId); -+async function ensureWorkPeriodById (workPeriodId) { -+ return models.WorkPeriod.findById(workPeriodId) - } - - /** -@@ -1228,24 +1257,24 @@ async function ensureWorkPeriodById(workPeriodId) { - * @param {String} jobId the user id - * @returns {Object} the user data - */ --async function ensureUserById(userId) { -- const token = await getM2MUbahnToken(); -+async function ensureUserById (userId) { -+ const token = await getM2MUbahnToken() - try { - const res = await request - .get(`${config.TC_API}/users/${userId}`) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'ensureUserById', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return res.body; -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return res.body - } catch (err) { - if (err.status === HttpStatus.NOT_FOUND) { -- throw new errors.NotFoundError(`id: ${userId} "user" not found`); -+ throw new errors.NotFoundError(`id: ${userId} "user" not found`) - } -- throw err; -+ throw err - } - } - -@@ -1254,12 +1283,12 @@ async function ensureUserById(userId) { - * - * @returns {Object} the M2M auth user - */ --function getAuditM2Muser() { -+function getAuditM2Muser () { - return { - isMachine: true, - userId: config.m2m.M2M_AUDIT_USER_ID, -- handle: config.m2m.M2M_AUDIT_HANDLE, -- }; -+ handle: config.m2m.M2M_AUDIT_HANDLE -+ } - } - - /** -@@ -1271,24 +1300,24 @@ function getAuditM2Muser() { - * @param {Number} projectId project id - * @returns the result - */ --async function checkIsMemberOfProject(userId, projectId) { -- const m2mToken = await getM2MToken(); -+async function checkIsMemberOfProject (userId, projectId) { -+ const m2mToken = await getM2MToken() - const res = await request - .get(`${config.TC_API}/projects/${projectId}`) - .set('Authorization', `Bearer ${m2mToken}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -- const memberIdList = _.map(res.body.members, 'userId'); -+ .set('Accept', 'application/json') -+ const memberIdList = _.map(res.body.members, 'userId') - localLogger.debug({ - context: 'checkIsMemberOfProject', - message: `the members of project ${projectId}: ${JSON.stringify( - memberIdList -- )}, authUserId: ${JSON.stringify(userId)}`, -- }); -+ )}, authUserId: ${JSON.stringify(userId)}` -+ }) - if (!memberIdList.includes(userId)) { - throw new errors.UnauthorizedError( - `userId: ${userId} the user is not a member of project ${projectId}` -- ); -+ ) - } - } - -@@ -1298,11 +1327,11 @@ async function checkIsMemberOfProject(userId, projectId) { - * @param {Array} handles the array of handles - * @returns {Array} the member details - */ --async function getMemberDetailsByHandles(handles) { -+async function getMemberDetailsByHandles (handles) { - if (!handles.length) { -- return []; -+ return [] - } -- const token = await getM2MToken(); -+ const token = await getM2MToken() - const res = await request - .get(`${config.TOPCODER_MEMBERS_API}/_search`) - .query({ -@@ -1310,15 +1339,15 @@ async function getMemberDetailsByHandles(handles) { - handles, - (handle) => `handleLower:${handle.toLowerCase()}` - ).join(' OR '), -- fields: 'userId,handle,firstName,lastName,email', -+ fields: 'userId,handle,firstName,lastName,email' - }) - .set('Authorization', `Bearer ${token}`) -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getMemberDetailsByHandles', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return _.get(res.body, 'result.content'); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return _.get(res.body, 'result.content') - } - - /** -@@ -1327,17 +1356,17 @@ async function getMemberDetailsByHandles(handles) { - * @param {String} handle the user handle - * @returns {Object} the member details - */ --async function getV3MemberDetailsByHandle(handle) { -- const token = await getM2MToken(); -+async function getV3MemberDetailsByHandle (handle) { -+ const token = await getM2MToken() - const res = await request - .get(`${config.TOPCODER_MEMBERS_API}/${handle}`) - .set('Authorization', `Bearer ${token}`) -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getV3MemberDetailsByHandle', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return _.get(res.body, 'result.content'); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return _.get(res.body, 'result.content') - } - - /** -@@ -1347,20 +1376,20 @@ async function getV3MemberDetailsByHandle(handle) { - * @param {String} email the email - * @returns {Array} the member details - */ --async function _getMemberDetailsByEmail(token, email) { -+async function _getMemberDetailsByEmail (token, email) { - const res = await request - .get(config.TOPCODER_USERS_API) - .query({ - filter: `email=${email}`, -- fields: 'handle,id,email,firstName,lastName', -+ fields: 'handle,id,email,firstName,lastName' - }) - .set('Authorization', `Bearer ${token}`) -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: '_getMemberDetailsByEmail', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return _.get(res.body, 'result.content'); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return _.get(res.body, 'result.content') - } - - /** -@@ -1370,25 +1399,25 @@ async function _getMemberDetailsByEmail(token, email) { - * @param {Array} emails the array of emails - * @returns {Array} the member details - */ --async function getMemberDetailsByEmails(emails) { -- const token = await getM2MToken(); -+async function getMemberDetailsByEmails (emails) { -+ const token = await getM2MToken() - const limiter = new Bottleneck({ -- maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API, -- }); -+ maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API -+ }) - const membersArray = await Promise.all( - emails.map((email) => - limiter.schedule(() => - _getMemberDetailsByEmail(token, email).catch((error) => { - localLogger.error({ - context: 'getMemberDetailsByEmails', -- message: error.message, -- }); -- return []; -+ message: error.message -+ }) -+ return [] - }) - ) - ) -- ); -- return _.flatten(membersArray); -+ ) -+ return _.flatten(membersArray) - } - - /** -@@ -1399,20 +1428,20 @@ async function getMemberDetailsByEmails(emails) { - * @param {Object} criteria the filtering criteria - * @returns {Object} the member created - */ --async function createProjectMember(projectId, data, criteria) { -- const m2mToken = await getM2MToken(); -+async function createProjectMember (projectId, data, criteria) { -+ const m2mToken = await getM2MToken() - const { body: member } = await request - .post(`${config.TC_API}/projects/${projectId}/members`) - .set('Authorization', `Bearer ${m2mToken}`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - .query(criteria) -- .send(data); -+ .send(data) - localLogger.debug({ - context: 'createProjectMember', -- message: `response body: ${JSON.stringify(member)}`, -- }); -- return member; -+ message: `response body: ${JSON.stringify(member)}` -+ }) -+ return member - } - - /** -@@ -1422,21 +1451,21 @@ async function createProjectMember(projectId, data, criteria) { - * @param {Object} criteria the search criteria - * @returns {Array} the project members - */ --async function listProjectMembers(currentUser, projectId, criteria = {}) { -+async function listProjectMembers (currentUser, projectId, criteria = {}) { - const token = - currentUser.hasManagePermission || currentUser.isMachine - ? `Bearer ${await getM2MToken()}` -- : currentUser.jwtToken; -+ : currentUser.jwtToken - const { body: members } = await request - .get(`${config.TC_API}/projects/${projectId}/members`) - .query(criteria) - .set('Authorization', token) -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'listProjectMembers', -- message: `response body: ${JSON.stringify(members)}`, -- }); -- return members; -+ message: `response body: ${JSON.stringify(members)}` -+ }) -+ return members - } - - /** -@@ -1446,21 +1475,21 @@ async function listProjectMembers(currentUser, projectId, criteria = {}) { - * @param {Object} criteria the search criteria - * @returns {Array} the member invites - */ --async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { -+async function listProjectMemberInvites (currentUser, projectId, criteria = {}) { - const token = - currentUser.hasManagePermission || currentUser.isMachine - ? `Bearer ${await getM2MToken()}` -- : currentUser.jwtToken; -+ : currentUser.jwtToken - const { body: invites } = await request - .get(`${config.TC_API}/projects/${projectId}/invites`) - .query(criteria) - .set('Authorization', token) -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'listProjectMemberInvites', -- message: `response body: ${JSON.stringify(invites)}`, -- }); -- return invites; -+ message: `response body: ${JSON.stringify(invites)}` -+ }) -+ return invites - } - - /** -@@ -1470,24 +1499,24 @@ async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { - * @param {String} projectMemberId the id of the project member - * @returns {undefined} - */ --async function deleteProjectMember(currentUser, projectId, projectMemberId) { -+async function deleteProjectMember (currentUser, projectId, projectMemberId) { - const token = - currentUser.hasManagePermission || currentUser.isMachine - ? `Bearer ${await getM2MToken()}` -- : currentUser.jwtToken; -+ : currentUser.jwtToken - try { - await request - .delete( - `${config.TC_API}/projects/${projectId}/members/${projectMemberId}` - ) -- .set('Authorization', token); -+ .set('Authorization', token) - } catch (err) { - if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError( - `projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}` -- ); -+ ) - } -- throw err; -+ throw err - } - } - -@@ -1497,13 +1526,13 @@ async function deleteProjectMember(currentUser, projectId, projectMemberId) { - * @param {String} attributeName Requested attribute name, e.g. "email" - * @returns attribute value - */ --function getUserAttributeValue(user, attributeName) { -- const attributes = _.get(user, 'attributes', []); -+function getUserAttributeValue (user, attributeName) { -+ const attributes = _.get(user, 'attributes', []) - const targetAttribute = _.find( - attributes, - (a) => a.attribute.name === attributeName -- ); -- return _.get(targetAttribute, 'value'); -+ ) -+ return _.get(targetAttribute, 'value') - } - - /** -@@ -1513,34 +1542,34 @@ function getUserAttributeValue(user, attributeName) { - * @param {String} token m2m token - * @returns {Object} the challenge created - */ --async function createChallenge(data, token) { -+async function createChallenge (data, token) { - if (!token) { -- token = await getM2MToken(); -+ token = await getM2MToken() - } -- const url = `${config.TC_API}/challenges`; -+ const url = `${config.TC_API}/challenges` - localLogger.debug({ - context: 'createChallenge', -- message: `EndPoint: POST ${url}`, -- }); -+ message: `EndPoint: POST ${url}` -+ }) - localLogger.debug({ - context: 'createChallenge', -- message: `Request Body: ${JSON.stringify(data)}`, -- }); -+ message: `Request Body: ${JSON.stringify(data)}` -+ }) - const { body: challenge, status: httpStatus } = await request - .post(url) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') -- .send(data); -+ .send(data) - localLogger.debug({ - context: 'createChallenge', -- message: `Status Code: ${httpStatus}`, -- }); -+ message: `Status Code: ${httpStatus}` -+ }) - localLogger.debug({ - context: 'createChallenge', -- message: `Response Body: ${JSON.stringify(challenge)}`, -- }); -- return challenge; -+ message: `Response Body: ${JSON.stringify(challenge)}` -+ }) -+ return challenge - } - - /** -@@ -1551,34 +1580,34 @@ async function createChallenge(data, token) { - * @param {String} token m2m token - * @returns {Object} the challenge updated - */ --async function updateChallenge(challengeId, data, token) { -+async function updateChallenge (challengeId, data, token) { - if (!token) { -- token = await getM2MToken(); -+ token = await getM2MToken() - } -- const url = `${config.TC_API}/challenges/${challengeId}`; -+ const url = `${config.TC_API}/challenges/${challengeId}` - localLogger.debug({ - context: 'updateChallenge', -- message: `EndPoint: PATCH ${url}`, -- }); -+ message: `EndPoint: PATCH ${url}` -+ }) - localLogger.debug({ - context: 'updateChallenge', -- message: `Request Body: ${JSON.stringify(data)}`, -- }); -+ message: `Request Body: ${JSON.stringify(data)}` -+ }) - const { body: challenge, status: httpStatus } = await request - .patch(url) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') -- .send(data); -+ .send(data) - localLogger.debug({ - context: 'updateChallenge', -- message: `Status Code: ${httpStatus}`, -- }); -+ message: `Status Code: ${httpStatus}` -+ }) - localLogger.debug({ - context: 'updateChallenge', -- message: `Response Body: ${JSON.stringify(challenge)}`, -- }); -- return challenge; -+ message: `Response Body: ${JSON.stringify(challenge)}` -+ }) -+ return challenge - } - - /** -@@ -1588,34 +1617,34 @@ async function updateChallenge(challengeId, data, token) { - * @param {String} token m2m token - * @returns {Object} the resource created - */ --async function createChallengeResource(data, token) { -+async function createChallengeResource (data, token) { - if (!token) { -- token = await getM2MToken(); -+ token = await getM2MToken() - } -- const url = `${config.TC_API}/resources`; -+ const url = `${config.TC_API}/resources` - localLogger.debug({ - context: 'createChallengeResource', -- message: `EndPoint: POST ${url}`, -- }); -+ message: `EndPoint: POST ${url}` -+ }) - localLogger.debug({ - context: 'createChallengeResource', -- message: `Request Body: ${JSON.stringify(data)}`, -- }); -+ message: `Request Body: ${JSON.stringify(data)}` -+ }) - const { body: resource, status: httpStatus } = await request - .post(url) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') -- .send(data); -+ .send(data) - localLogger.debug({ - context: 'createChallengeResource', -- message: `Status Code: ${httpStatus}`, -- }); -+ message: `Status Code: ${httpStatus}` -+ }) - localLogger.debug({ - context: 'createChallengeResource', -- message: `Response Body: ${JSON.stringify(resource)}`, -- }); -- return resource; -+ message: `Response Body: ${JSON.stringify(resource)}` -+ }) -+ return resource - } - - /** -@@ -1624,40 +1653,40 @@ async function createChallengeResource(data, token) { - * @param {Date} end end date of the resource booking - * @returns {Array<{startDate:Date, endDate:Date, daysWorked:number}>} information about workPeriods - */ --function extractWorkPeriods(start, end) { -+function extractWorkPeriods (start, end) { - // calculate maximum possible daysWorked for a week -- function getDaysWorked(week) { -+ function getDaysWorked (week) { - if (weeks === 1) { -- return Math.min(endDay, 5) - Math.max(startDay, 1) + 1; -+ return Math.min(endDay, 5) - Math.max(startDay, 1) + 1 - } else if (week === 0) { -- return Math.min(6 - startDay, 5); -+ return Math.min(6 - startDay, 5) - } else if (week === weeks - 1) { -- return Math.min(endDay, 5); -- } else return 5; -+ return Math.min(endDay, 5) -+ } else return 5 - } -- const periods = []; -+ const periods = [] - if (_.isNil(start) || _.isNil(end)) { -- return periods; -+ return periods - } -- const startDate = moment(start); -- const startDay = startDate.get('day'); -- startDate.set('day', 0).startOf('day'); -+ const startDate = moment(start) -+ const startDay = startDate.get('day') -+ startDate.set('day', 0).startOf('day') - -- const endDate = moment(end); -- const endDay = endDate.get('day'); -- endDate.set('day', 6).endOf('day'); -+ const endDate = moment(end) -+ const endDay = endDate.get('day') -+ endDate.set('day', 6).endOf('day') - -- const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7; -+ const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7 - - for (let i = 0; i < weeks; i++) { - periods.push({ - startDate: startDate.format('YYYY-MM-DD'), - endDate: startDate.add(6, 'day').format('YYYY-MM-DD'), -- daysWorked: getDaysWorked(i), -- }); -- startDate.add(1, 'day'); -+ daysWorked: getDaysWorked(i) -+ }) -+ startDate.add(1, 'day') - } -- return periods; -+ return periods - } - - /** -@@ -1666,19 +1695,19 @@ function extractWorkPeriods(start, end) { - * @param {String} userHandle user handle - * @returns {String} email address of the user - */ --async function getUserByHandle(userHandle) { -- const token = await getM2MToken(); -- const url = `${config.TC_API}/members/${userHandle}`; -+async function getUserByHandle (userHandle) { -+ const token = await getM2MToken() -+ const url = `${config.TC_API}/members/${userHandle}` - const res = await request - .get(url) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getUserByHandle', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return _.get(res, 'body'); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return _.get(res, 'body') - } - - /** -@@ -1687,14 +1716,14 @@ async function getUserByHandle(userHandle) { - * @param {*} object of json that would be replaced in string - * @returns - */ --async function substituteStringByObject(string, object) { -+async function substituteStringByObject (string, object) { - for (var key in object) { - if (!Object.prototype.hasOwnProperty.call(object, key)) { -- continue; -+ continue - } -- string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]); -+ string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]) - } -- return string; -+ return string - } - - /** -@@ -1702,19 +1731,19 @@ async function substituteStringByObject(string, object) { - * @param {Object} data title of project and any other info - * @returns {Object} the project created - */ --async function createProject(currentUser, data) { -- const token = currentUser.jwtToken; -+async function createProject (currentUser, data) { -+ const token = currentUser.jwtToken - const res = await request - .post(`${config.TC_API}/projects/`) - .set('Authorization', token) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') -- .send(data); -+ .send(data) - localLogger.debug({ - context: 'createProject', -- message: `response body: ${JSON.stringify(res)}`, -- }); -- return _.get(res, 'body'); -+ message: `response body: ${JSON.stringify(res)}` -+ }) -+ return _.get(res, 'body') - } - - module.exports = { -@@ -1733,9 +1762,9 @@ module.exports = { - getUserId: async (userId) => { - // check m2m user id - if (userId === config.m2m.M2M_AUDIT_USER_ID) { -- return config.m2m.M2M_AUDIT_USER_ID; -+ return config.m2m.M2M_AUDIT_USER_ID - } -- return ensureUbahnUserId({ userId }); -+ return ensureUbahnUserId({ userId }) - }, - getUserByExternalId, - getM2MToken, -@@ -1769,5 +1798,5 @@ module.exports = { - extractWorkPeriods, - getUserByHandle, - substituteStringByObject, -- createProject, --}; -+ createProject -+} -diff --git a/src/controllers/RoleController.js b/src/controllers/RoleController.js -new file mode 100644 -index 0000000..747cbe4 ---- /dev/null -+++ b/src/controllers/RoleController.js -@@ -0,0 +1,59 @@ -+/** -+ * Controller for Role endpoints -+ */ -+const HttpStatus = require('http-status-codes') -+const service = require('../services/RoleService') -+ -+/** -+ * Get role by id -+ * @param req the request -+ * @param res the response -+ */ -+async function getRole (req, res) { -+ res.send(await service.getRole(req.authUser, req.params.id, req.query.fromDb)) -+} -+ -+/** -+ * Create role -+ * @param req the request -+ * @param res the response -+ */ -+async function createRole (req, res) { -+ res.send(await service.createRole(req.authUser, req.body)) -+} -+ -+/** -+ * update role by id -+ * @param req the request -+ * @param res the response -+ */ -+async function updateRole (req, res) { -+ res.send(await service.updateRole(req.authUser, req.params.id, req.body)) -+} -+ -+/** -+ * Delete role by id -+ * @param req the request -+ * @param res the response -+ */ -+async function deleteRole (req, res) { -+ await service.deleteRole(req.authUser, req.params.id) -+ res.status(HttpStatus.NO_CONTENT).end() -+} -+ -+/** -+ * Search roles -+ * @param req the request -+ * @param res the response -+ */ -+async function searchRoles (req, res) { -+ res.send(await service.searchRoles(req.authUser, req.query)) -+} -+ -+module.exports = { -+ getRole, -+ createRole, -+ updateRole, -+ deleteRole, -+ searchRoles -+} -diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js -index ca4f1bc..26d7073 100644 ---- a/src/controllers/TeamController.js -+++ b/src/controllers/TeamController.js -@@ -1,19 +1,19 @@ - /** - * Controller for TaaS teams endpoints - */ --const HttpStatus = require('http-status-codes'); --const service = require('../services/TeamService'); --const helper = require('../common/helper'); -+const HttpStatus = require('http-status-codes') -+const service = require('../services/TeamService') -+const helper = require('../common/helper') - - /** - * Search teams - * @param req the request - * @param res the response - */ --async function searchTeams(req, res) { -- const result = await service.searchTeams(req.authUser, req.query); -- helper.setResHeaders(req, res, result); -- res.send(result.result); -+async function searchTeams (req, res) { -+ const result = await service.searchTeams(req.authUser, req.query) -+ helper.setResHeaders(req, res, result) -+ res.send(result.result) - } - - /** -@@ -21,8 +21,8 @@ async function searchTeams(req, res) { - * @param req the request - * @param res the response - */ --async function getTeam(req, res) { -- res.send(await service.getTeam(req.authUser, req.params.id)); -+async function getTeam (req, res) { -+ res.send(await service.getTeam(req.authUser, req.params.id)) - } - - /** -@@ -30,10 +30,10 @@ async function getTeam(req, res) { - * @param req the request - * @param res the response - */ --async function getTeamJob(req, res) { -+async function getTeamJob (req, res) { - res.send( - await service.getTeamJob(req.authUser, req.params.id, req.params.jobId) -- ); -+ ) - } - - /** -@@ -41,9 +41,9 @@ async function getTeamJob(req, res) { - * @param req the request - * @param res the response - */ --async function sendEmail(req, res) { -- await service.sendEmail(req.authUser, req.body); -- res.status(HttpStatus.NO_CONTENT).end(); -+async function sendEmail (req, res) { -+ await service.sendEmail(req.authUser, req.body) -+ res.status(HttpStatus.NO_CONTENT).end() - } - - /** -@@ -51,10 +51,10 @@ async function sendEmail(req, res) { - * @param req the request - * @param res the response - */ --async function addMembers(req, res) { -+async function addMembers (req, res) { - res.send( - await service.addMembers(req.authUser, req.params.id, req.query, req.body) -- ); -+ ) - } - - /** -@@ -62,13 +62,13 @@ async function addMembers(req, res) { - * @param req the request - * @param res the response - */ --async function searchMembers(req, res) { -+async function searchMembers (req, res) { - const result = await service.searchMembers( - req.authUser, - req.params.id, - req.query -- ); -- res.send(result.result); -+ ) -+ res.send(result.result) - } - - /** -@@ -76,13 +76,13 @@ async function searchMembers(req, res) { - * @param req the request - * @param res the response - */ --async function searchInvites(req, res) { -+async function searchInvites (req, res) { - const result = await service.searchInvites( - req.authUser, - req.params.id, - req.query -- ); -- res.send(result.result); -+ ) -+ res.send(result.result) - } - - /** -@@ -90,13 +90,13 @@ async function searchInvites(req, res) { - * @param req the request - * @param res the response - */ --async function deleteMember(req, res) { -+async function deleteMember (req, res) { - await service.deleteMember( - req.authUser, - req.params.id, - req.params.projectMemberId -- ); -- res.status(HttpStatus.NO_CONTENT).end(); -+ ) -+ res.status(HttpStatus.NO_CONTENT).end() - } - - /** -@@ -104,8 +104,8 @@ async function deleteMember(req, res) { - * @param req the request - * @param res the response - */ --async function getMe(req, res) { -- res.send(await service.getMe(req.authUser)); -+async function getMe (req, res) { -+ res.send(await service.getMe(req.authUser)) - } - - /** -@@ -113,8 +113,8 @@ async function getMe(req, res) { - * @param req the request - * @param res the response - */ --async function createProj(req, res) { -- res.send(await service.createProj(req.authUser, req.body)); -+async function createProj (req, res) { -+ res.send(await service.createProj(req.authUser, req.body)) - } - - module.exports = { -@@ -127,5 +127,5 @@ module.exports = { - searchInvites, - deleteMember, - getMe, -- createProj, --}; -+ createProj -+} -diff --git a/src/eventHandlers/RoleEventHandler.js b/src/eventHandlers/RoleEventHandler.js -new file mode 100644 -index 0000000..38dbdb7 ---- /dev/null -+++ b/src/eventHandlers/RoleEventHandler.js -@@ -0,0 +1,64 @@ -+/* -+ * Handle events for ResourceBooking. -+ */ -+ -+const { Op } = require('sequelize') -+const _ = require('lodash') -+const models = require('../models') -+const logger = require('../common/logger') -+const helper = require('../common/helper') -+const JobService = require('../services/JobService') -+ -+const Job = models.Job -+ -+/** -+ * When a Role is deleted, jobs related to -+ * that role should be updated -+ * @param {object} payload the event payload -+ * @returns {undefined} -+ */ -+async function updateJobs (payload) { -+ // find jobs have this role -+ const jobs = await Job.findAll({ -+ where: { -+ roleIds: { [Op.contains]: [payload.value.id] } -+ }, -+ raw: true -+ }) -+ if (jobs.length === 0) { -+ logger.debug({ -+ component: 'RoleEventHandler', -+ context: 'updateJobs', -+ message: `id: ${payload.value.id} role has no related job - ignored` -+ }) -+ return -+ } -+ const m2mUser = helper.getAuditM2Muser() -+ // remove role id from related jobs -+ await Promise.all(_.map(jobs, async job => { -+ let roleIds = _.filter(job.roleIds, roleId => roleId !== payload.value.id) -+ if (roleIds.length === 0) { -+ roleIds = null -+ } -+ await JobService.partiallyUpdateJob(m2mUser, job.id, { roleIds }) -+ })) -+ logger.debug({ -+ component: 'RoleEventHandler', -+ context: 'updateJobs', -+ message: `role id: ${payload.value.id} removed from jobs with id: ${_.map(jobs, 'id')}` -+ }) -+} -+ -+/** -+ * Process role delete event. -+ * -+ * @param {Object} payload the event payload -+ * @returns {undefined} -+ */ -+async function processDelete (payload) { -+ await updateJobs(payload) -+} -+ -+module.exports = { -+ processDelete -+} -diff --git a/src/eventHandlers/index.js b/src/eventHandlers/index.js -index 1744599..6e0ec2a 100644 ---- a/src/eventHandlers/index.js -+++ b/src/eventHandlers/index.js -@@ -8,6 +8,7 @@ const JobEventHandler = require('./JobEventHandler') - const JobCandidateEventHandler = require('./JobCandidateEventHandler') - const ResourceBookingEventHandler = require('./ResourceBookingEventHandler') - const InterviewEventHandler = require('./InterviewEventHandler') -+const RoleEventHandler = require('./RoleEventHandler') - const logger = require('../common/logger') - - const TopicOperationMapping = { -@@ -16,7 +17,8 @@ const TopicOperationMapping = { - [config.TAAS_RESOURCE_BOOKING_CREATE_TOPIC]: ResourceBookingEventHandler.processCreate, - [config.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC]: ResourceBookingEventHandler.processUpdate, - [config.TAAS_RESOURCE_BOOKING_DELETE_TOPIC]: ResourceBookingEventHandler.processDelete, -- [config.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewEventHandler.processRequest -+ [config.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewEventHandler.processRequest, -+ [config.TAAS_ROLE_DELETE_TOPIC]: RoleEventHandler.processDelete - } - - /** -diff --git a/src/models/Job.js b/src/models/Job.js -index 49d34ff..66f15b0 100644 ---- a/src/models/Job.js -+++ b/src/models/Job.js -@@ -104,6 +104,12 @@ module.exports = (sequelize) => { - defaultValue: false, - allowNull: false - }, -+ roleIds: { -+ field: 'role_ids', -+ type: Sequelize.ARRAY({ -+ type: Sequelize.UUID -+ }) -+ }, - createdBy: { - field: 'created_by', - type: Sequelize.UUID, -diff --git a/src/models/Role.js b/src/models/Role.js -new file mode 100644 -index 0000000..57cd502 ---- /dev/null -+++ b/src/models/Role.js -@@ -0,0 +1,165 @@ -+const { Sequelize, Model } = require('sequelize') -+const config = require('config') -+const errors = require('../common/errors') -+ -+module.exports = (sequelize) => { -+ class Role extends Model { -+ /** -+ * Get role by id -+ * @param {String} id the role id -+ * @returns {Role} the role instance -+ */ -+ static async findById (id) { -+ const role = await Role.findOne({ -+ where: { -+ id -+ } -+ }) -+ if (!role) { -+ throw new errors.NotFoundError(`id: ${id} "Role" doesn't exists.`) -+ } -+ return role -+ } -+ } -+ Role.init( -+ { -+ id: { -+ type: Sequelize.UUID, -+ primaryKey: true, -+ allowNull: false, -+ defaultValue: Sequelize.UUIDV4 -+ }, -+ name: { -+ type: Sequelize.STRING(50), -+ allowNull: false -+ }, -+ description: { -+ type: Sequelize.STRING(1000) -+ }, -+ listOfSkills: { -+ field: 'list_of_skills', -+ type: Sequelize.ARRAY({ -+ type: Sequelize.STRING(50) -+ }) -+ }, -+ rates: { -+ type: Sequelize.ARRAY({ -+ type: Sequelize.JSONB({ -+ global: { -+ type: Sequelize.SMALLINT, -+ allowNull: false -+ }, -+ inCountry: { -+ field: 'in_country', -+ type: Sequelize.SMALLINT, -+ allowNull: false -+ }, -+ offShore: { -+ field: 'off_shore', -+ type: Sequelize.SMALLINT, -+ allowNull: false -+ }, -+ rate30Global: { -+ field: 'rate30_global', -+ type: Sequelize.SMALLINT -+ }, -+ rate30InCountry: { -+ field: 'rate30_in_country', -+ type: Sequelize.SMALLINT -+ }, -+ rate30OffShore: { -+ field: 'rate30_off_shore', -+ type: Sequelize.SMALLINT -+ }, -+ rate20Global: { -+ field: 'rate20_global', -+ type: Sequelize.SMALLINT -+ }, -+ rate20InCountry: { -+ field: 'rate20_in_country', -+ type: Sequelize.SMALLINT -+ }, -+ rate20OffShore: { -+ field: 'rate20_off_shore', -+ type: Sequelize.SMALLINT -+ } -+ }), -+ allowNull: false -+ }), -+ allowNull: false -+ }, -+ numberOfMembers: { -+ field: 'number_of_members', -+ type: Sequelize.NUMERIC -+ }, -+ numberOfMembersAvailable: { -+ field: 'number_of_members_available', -+ type: Sequelize.SMALLINT -+ }, -+ imageUrl: { -+ field: 'image_url', -+ type: Sequelize.STRING(255) -+ }, -+ timeToCandidate: { -+ field: 'time_to_candidate', -+ type: Sequelize.SMALLINT -+ }, -+ timeToInterview: { -+ field: 'time_to_interview', -+ type: Sequelize.SMALLINT -+ }, -+ createdBy: { -+ field: 'created_by', -+ type: Sequelize.UUID, -+ allowNull: false -+ }, -+ updatedBy: { -+ field: 'updated_by', -+ type: Sequelize.UUID -+ }, -+ createdAt: { -+ field: 'created_at', -+ type: Sequelize.DATE -+ }, -+ updatedAt: { -+ field: 'updated_at', -+ type: Sequelize.DATE -+ }, -+ deletedAt: { -+ field: 'deleted_at', -+ type: Sequelize.DATE -+ } -+ }, -+ { -+ schema: config.DB_SCHEMA_NAME, -+ sequelize, -+ tableName: 'roles', -+ paranoid: true, -+ deletedAt: 'deletedAt', -+ createdAt: 'createdAt', -+ updatedAt: 'updatedAt', -+ timestamps: true, -+ defaultScope: { -+ attributes: { -+ exclude: ['deletedAt'] -+ } -+ }, -+ hooks: { -+ afterCreate: (role) => { -+ delete role.dataValues.deletedAt -+ } -+ }, -+ indexes: [ -+ { -+ unique: true, -+ fields: ['name'], -+ where: { -+ deleted_at: null -+ } -+ } -+ ] -+ } -+ ) -+ -+ return Role -+} -diff --git a/src/routes/RoleRoutes.js b/src/routes/RoleRoutes.js -new file mode 100644 -index 0000000..2fb6d55 ---- /dev/null -+++ b/src/routes/RoleRoutes.js -@@ -0,0 +1,41 @@ -+/** -+ * Contains role routes -+ */ -+const constants = require('../../app-constants') -+ -+module.exports = { -+ '/roles': { -+ post: { -+ controller: 'RoleController', -+ method: 'createRole', -+ auth: 'jwt', -+ scopes: [constants.Scopes.CREATE_ROLE, constants.Scopes.ALL_ROLE] -+ }, -+ get: { -+ controller: 'RoleController', -+ method: 'searchRoles', -+ auth: 'jwt', -+ scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] -+ } -+ }, -+ '/roles/:id': { -+ get: { -+ controller: 'RoleController', -+ method: 'getRole', -+ auth: 'jwt', -+ scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] -+ }, -+ patch: { -+ controller: 'RoleController', -+ method: 'updateRole', -+ auth: 'jwt', -+ scopes: [constants.Scopes.UPDATE_ROLE, constants.Scopes.ALL_ROLE] -+ }, -+ delete: { -+ controller: 'RoleController', -+ method: 'deleteRole', -+ auth: 'jwt', -+ scopes: [constants.Scopes.DELETE_ROLE, constants.Scopes.ALL_ROLE] -+ } -+ } -+} -diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js -index 9bbe25c..07d777d 100644 ---- a/src/routes/TeamRoutes.js -+++ b/src/routes/TeamRoutes.js -@@ -1,7 +1,7 @@ - /** - * Contains taas team routes - */ --const constants = require('../../app-constants'); -+const constants = require('../../app-constants') - - module.exports = { - '/taas-teams': { -@@ -9,85 +9,85 @@ module.exports = { - controller: 'TeamController', - method: 'searchTeams', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/email': { - post: { - controller: 'TeamController', - method: 'sendEmail', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/skills': { - get: { - controller: 'SkillController', - method: 'searchSkills', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/me': { - get: { - controller: 'TeamController', - method: 'getMe', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/:id': { - get: { - controller: 'TeamController', - method: 'getTeam', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/:id/jobs/:jobId': { - get: { - controller: 'TeamController', - method: 'getTeamJob', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/:id/members': { - post: { - controller: 'TeamController', - method: 'addMembers', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -+ scopes: [constants.Scopes.READ_TAAS_TEAM] - }, - get: { - controller: 'TeamController', - method: 'searchMembers', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/:id/invites': { - get: { - controller: 'TeamController', - method: 'searchInvites', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/:id/members/:projectMemberId': { - delete: { - controller: 'TeamController', - method: 'deleteMember', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/createTeamRequest': { - post: { - controller: 'TeamController', - method: 'createProj', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -- }, --}; -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } -+ } -+} -diff --git a/src/services/InterviewService.js b/src/services/InterviewService.js -index 10a065f..a69a788 100644 ---- a/src/services/InterviewService.js -+++ b/src/services/InterviewService.js -@@ -241,8 +241,8 @@ async function requestInterview (currentUser, jobCandidateId, interview) { - const guestMembers = await helper.getMemberDetailsByEmails(interview.guestEmails) - interview.hostName = `${hostMembers[0].firstName} ${hostMembers[0].lastName}` - interview.guestNames = _.map(interview.guestEmails, (guestEmail) => { -- var foundGuestMember = _.find(guestMembers, function(guestMember) { return guestEmail == guestMember.email }); -- return (foundGuestMember != undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split("@")[0] -+ var foundGuestMember = _.find(guestMembers, function (guestMember) { return guestEmail === guestMember.email }) -+ return (foundGuestMember !== undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split('@')[0] - }) - - try { -diff --git a/src/services/JobService.js b/src/services/JobService.js -index 7d855bd..be5dfde 100644 ---- a/src/services/JobService.js -+++ b/src/services/JobService.js -@@ -74,6 +74,27 @@ async function _validateSkills (skills) { - } - } - -+/** -+ * Validate if all roles exist. -+ * -+ * @param {Array} roles the list of roles -+ * @returns {undefined} -+ */ -+async function _validateRoles (roles) { -+ const foundRolesObj = await models.Role.findAll({ -+ where: { -+ id: roles -+ }, -+ attributes: ['id'], -+ raw: true -+ }) -+ const foundRoles = _.map(foundRolesObj, 'id') -+ const nonexistentRoles = _.difference(roles, foundRoles) -+ if (nonexistentRoles.length > 0) { -+ throw new errors.BadRequestError(`Invalid roles: [${nonexistentRoles}]`) -+ } -+} -+ - /** - * Check user permission for getting job. - * -@@ -154,6 +175,10 @@ async function createJob (currentUser, job) { - } - - await _validateSkills(job.skills) -+ if (job.roleIds) { -+ job.roleIds = _.uniq(job.roleIds) -+ await _validateRoles(job.roleIds) -+ } - job.id = uuid() - job.createdBy = await helper.getUserId(currentUser.userId) - -@@ -177,7 +202,8 @@ createJob.schema = Joi.object().keys({ - rateType: Joi.rateType().allow(null), - workload: Joi.workload().allow(null), - skills: Joi.array().items(Joi.string().uuid()).required(), -- isApplicationPageActive: Joi.boolean() -+ isApplicationPageActive: Joi.boolean(), -+ roleIds: Joi.array().items(Joi.string().uuid().required()) - }).required() - }).required() - -@@ -192,6 +218,10 @@ async function updateJob (currentUser, id, data) { - if (data.skills) { - await _validateSkills(data.skills) - } -+ if (data.roleIds) { -+ data.roleIds = _.uniq(data.roleIds) -+ await _validateRoles(data.roleIds) -+ } - let job = await Job.findById(id) - const oldValue = job.toJSON() - -@@ -245,7 +275,8 @@ partiallyUpdateJob.schema = Joi.object().keys({ - rateType: Joi.rateType().allow(null), - workload: Joi.workload().allow(null), - skills: Joi.array().items(Joi.string().uuid()), -- isApplicationPageActive: Joi.boolean() -+ isApplicationPageActive: Joi.boolean(), -+ roleIds: Joi.array().items(Joi.string().uuid().required()).allow(null) - }).required() - }).required() - -@@ -276,7 +307,8 @@ fullyUpdateJob.schema = Joi.object().keys({ - workload: Joi.workload().allow(null).default(null), - skills: Joi.array().items(Joi.string().uuid()).required(), - status: Joi.jobStatus().default('sourcing'), -- isApplicationPageActive: Joi.boolean() -+ isApplicationPageActive: Joi.boolean(), -+ roleIds: Joi.array().items(Joi.string().uuid().required()).default(null) - }).required() - }).required() - -@@ -444,9 +476,9 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } - [Op.like]: `%${criteria.title}%` - } - } -- if (criteria.skills) { -+ if (criteria.skill) { - filter.skills = { -- [Op.contains]: [criteria.skills] -+ [Op.contains]: [criteria.skill] - } - } - const jobs = await Job.findAll({ -diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js -index f5c4020..fd3d777 100644 ---- a/src/services/ResourceBookingService.js -+++ b/src/services/ResourceBookingService.js -@@ -1,3 +1,4 @@ -+/* eslint-disable no-unreachable */ - /** - * This service provides operations of ResourceBooking. - */ -diff --git a/src/services/RoleService.js b/src/services/RoleService.js -new file mode 100644 -index 0000000..19006f6 ---- /dev/null -+++ b/src/services/RoleService.js -@@ -0,0 +1,305 @@ -+/** -+ * This service provides operations of Roles. -+ */ -+ -+const _ = require('lodash') -+const config = require('config') -+const Joi = require('joi') -+const { Op } = require('sequelize') -+const uuid = require('uuid') -+const helper = require('../common/helper') -+const logger = require('../common/logger') -+const errors = require('../common/errors') -+const models = require('../models') -+ -+const Role = models.Role -+const esClient = helper.getESClient() -+ -+/** -+ * Check user permission for deleting, creating or updating role. -+ * @param {Object} currentUser the user who perform this operation. -+ * @returns {undefined} -+ */ -+async function _checkUserPermissionForWriteDeleteRole (currentUser) { -+ if (!currentUser.hasManagePermission && !currentUser.isMachine) { -+ throw new errors.ForbiddenError('You are not allowed to perform this action!') -+ } -+} -+ -+/** -+ * Cleans and validates skill names using skills service -+ * @param {Array} skills array of skill names to validate -+ * @returns {undefined} -+ */ -+async function _cleanAndValidateSkillNames (skills) { -+ // remove duplicates, leading and trailing whitespaces, remove empties and convert to lowercase. -+ const cleanedSkills = _.uniq(_.filter(_.map(skills, skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill))) -+ if (cleanedSkills.length > 0) { -+ // search skills if they are exists -+ const { result } = await helper.getTopcoderSkills({ name: _.join(cleanedSkills, ',') }) -+ const skillNames = _.map(result, 'name') -+ // find skills that not valid -+ const unValidSkills = _.differenceWith(cleanedSkills, skillNames, (a, b) => _.toLower(a) === _.toLower(b)) -+ if (unValidSkills.length > 0) { -+ throw new errors.BadRequestError(`skills: "${unValidSkills}" are not valid`) -+ } -+ return cleanedSkills -+ } else { -+ return null -+ } -+} -+ -+/** -+ * Check user permission for deleting, creating or updating role. -+ * @param {Object} currentUser the user who perform this operation. -+ * @returns {undefined} -+ */ -+async function _checkIfSameNamedRoleExists (roleName) { -+ // We can't create another Role with the same name -+ const role = await Role.findOne({ -+ where: { -+ name: { [Op.iLike]: roleName } -+ }, -+ raw: true -+ }) -+ if (role) { -+ throw new errors.BadRequestError(`Role: "${role.name}" is already exists.`) -+ } -+} -+ -+/** -+ * Get role by id -+ * @param {Object} currentUser the user who perform this operation. -+ * @param {String} id the role id -+ * @param {Boolean} fromDb flag if query db for data or not -+ * @returns {Object} the role -+ */ -+async function getRole (currentUser, id, fromDb = false) { -+ if (!fromDb) { -+ try { -+ const role = await esClient.get({ -+ index: config.esConfig.ES_INDEX_ROLE, -+ id -+ }) -+ return { id: role.body._id, ...role.body._source } -+ } catch (err) { -+ if (helper.isDocumentMissingException(err)) { -+ throw new errors.NotFoundError(`id: ${id} "Role" not found`) -+ } -+ } -+ } -+ logger.info({ component: 'RoleService', context: 'getRole', message: 'try to query db for data' }) -+ const role = await Role.findById(id) -+ -+ return role.toJSON() -+} -+ -+getRole.schema = Joi.object().keys({ -+ currentUser: Joi.object().required(), -+ id: Joi.string().uuid().required(), -+ fromDb: Joi.boolean() -+}).required() -+ -+/** -+ * Create role -+ * @param {Object} currentUser the user who perform this operation -+ * @param {Object} role the role to be created -+ * @returns {Object} the created role -+ */ -+async function createRole (currentUser, role) { -+ // check permission -+ await _checkUserPermissionForWriteDeleteRole(currentUser) -+ // check if another Role with the same name exists. -+ await _checkIfSameNamedRoleExists(role.name) -+ // clean and validate skill names -+ if (role.listOfSkills) { -+ role.listOfSkills = await _cleanAndValidateSkillNames(role.listOfSkills) -+ } -+ -+ role.id = uuid.v4() -+ role.createdBy = await helper.getUserId(currentUser.userId) -+ -+ const created = await Role.create(role) -+ -+ await helper.postEvent(config.TAAS_ROLE_CREATE_TOPIC, created.toJSON()) -+ return created.toJSON() -+} -+ -+createRole.schema = Joi.object().keys({ -+ currentUser: Joi.object().required(), -+ role: Joi.object().keys({ -+ name: Joi.string().max(50).required(), -+ description: Joi.string().max(1000), -+ listOfSkills: Joi.array().items(Joi.string().max(50).required()), -+ rates: Joi.array().items(Joi.object().keys({ -+ global: Joi.smallint().required(), -+ inCountry: Joi.smallint().required(), -+ offShore: Joi.smallint().required(), -+ rate30Global: Joi.smallint(), -+ rate30InCountry: Joi.smallint(), -+ rate30OffShore: Joi.smallint(), -+ rate20Global: Joi.smallint(), -+ rate20InCountry: Joi.smallint(), -+ rate20OffShore: Joi.smallint() -+ }).required()).required(), -+ numberOfMembers: Joi.number(), -+ numberOfMembersAvailable: Joi.smallint(), -+ imageUrl: Joi.string().uri().max(255), -+ timeToCandidate: Joi.smallint(), -+ timeToInterview: Joi.smallint() -+ }).required() -+}).required() -+ -+/** -+ * Partially Update role -+ * @param {Object} currentUser the user who perform this operation -+ * @param {String} id the role id -+ * @param {Object} data the data to be updated -+ * @returns {Object} the updated role -+ */ -+async function updateRole (currentUser, id, data) { -+ // check permission -+ await _checkUserPermissionForWriteDeleteRole(currentUser) -+ -+ const role = await Role.findById(id) -+ const oldValue = role.toJSON() -+ // if name is changed, check if another Role with the same name exists. -+ if (data.name && data.name.toLowerCase() !== role.dataValues.name.toLowerCase()) { -+ await _checkIfSameNamedRoleExists(data.name) -+ } -+ // clean and validate skill names -+ if (data.listOfSkills) { -+ data.listOfSkills = await _cleanAndValidateSkillNames(data.listOfSkills) -+ } -+ -+ data.updatedBy = await helper.getUserId(currentUser.userId) -+ const updated = await role.update(data) -+ -+ await helper.postEvent(config.TAAS_ROLE_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) -+ return updated.toJSON() -+} -+ -+updateRole.schema = Joi.object().keys({ -+ currentUser: Joi.object().required(), -+ id: Joi.string().uuid().required(), -+ data: Joi.object().keys({ -+ name: Joi.string().max(50), -+ description: Joi.string().max(1000).allow(null), -+ listOfSkills: Joi.array().items(Joi.string().max(50).required()).allow(null), -+ rates: Joi.array().items(Joi.object().keys({ -+ global: Joi.smallint().required(), -+ inCountry: Joi.smallint().required(), -+ offShore: Joi.smallint().required(), -+ rate30Global: Joi.smallint(), -+ rate30InCountry: Joi.smallint(), -+ rate30OffShore: Joi.smallint(), -+ rate20Global: Joi.smallint(), -+ rate20InCountry: Joi.smallint(), -+ rate20OffShore: Joi.smallint() -+ }).required()), -+ numberOfMembers: Joi.number().allow(null), -+ numberOfMembersAvailable: Joi.smallint().allow(null), -+ imageUrl: Joi.string().uri().max(255).allow(null), -+ timeToCandidate: Joi.smallint().allow(null), -+ timeToInterview: Joi.smallint().allow(null) -+ }).required() -+}).required() -+ -+/** -+ * Delete role by id -+ * @param {Object} currentUser the user who perform this operation -+ * @param {String} id the role id -+ */ -+async function deleteRole (currentUser, id) { -+ // check permission -+ await _checkUserPermissionForWriteDeleteRole(currentUser) -+ -+ const role = await Role.findById(id) -+ await role.destroy() -+ await helper.postEvent(config.TAAS_ROLE_DELETE_TOPIC, { id }) -+} -+ -+deleteRole.schema = Joi.object().keys({ -+ currentUser: Joi.object().required(), -+ id: Joi.string().uuid().required() -+}).required() -+ -+/** -+ * List roles -+ * @param {Object} currentUser the user who perform this operation. -+ * @param {Object} criteria the search criteria -+ * @returns {Object} the search result -+ */ -+async function searchRoles (currentUser, criteria) { -+ // clean skill names and convert into an array -+ criteria.skillsList = _.filter(_.map(_.split(_.trim(criteria.skillsList), ','), skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill)) -+ try { -+ const esQuery = { -+ index: config.get('esConfig.ES_INDEX_ROLE'), -+ body: { -+ query: { -+ bool: { -+ must: [] -+ } -+ }, -+ size: 10000, -+ sort: [{ name: { order: 'asc' } }] -+ } -+ } -+ // Apply skill name filters. listOfSkills array should include all skills provided in criteria. -+ _.each(criteria.skillsList, skill => { -+ esQuery.body.query.bool.must.push({ -+ term: { -+ listOfSkills: skill -+ } -+ }) -+ }) -+ // Apply name filter, allow partial match -+ if (criteria.keyword) { -+ esQuery.body.query.bool.must.push({ -+ wildcard: { -+ name: `*${criteria.keyword}*` -+ -+ } -+ }) -+ } -+ logger.debug({ component: 'RoleService', context: 'searchRoles', message: `Query: ${JSON.stringify(esQuery)}` }) -+ -+ const { body } = await esClient.search(esQuery) -+ return _.map(body.hits.hits, (hit) => _.assign(hit._source, { id: hit._id })) -+ } catch (err) { -+ logger.logFullError(err, { component: 'RoleService', context: 'searchRoles' }) -+ } -+ logger.info({ component: 'RoleService', context: 'searchRoles', message: 'fallback to DB query' }) -+ const filter = { [Op.and]: [] } -+ // Apply skill name filters. listOfSkills array should include all skills provided in criteria. -+ if (criteria.skillsList) { -+ filter[Op.and].push({ listOfSkills: { [Op.contains]: criteria.skillsList } }) -+ } -+ // Apply name filter, allow partial match and ignore case -+ if (criteria.keyword) { -+ filter[Op.and].push({ name: { [Op.iLike]: `%${criteria.keyword}%` } }) -+ } -+ const queryCriteria = { -+ where: filter, -+ order: [['name', 'asc']] -+ } -+ const roles = await Role.findAll(queryCriteria) -+ return roles -+} -+ -+searchRoles.schema = Joi.object().keys({ -+ currentUser: Joi.object().required(), -+ criteria: Joi.object().keys({ -+ skillsList: Joi.string(), -+ keyword: Joi.string() -+ }).required() -+}).required() -+ -+module.exports = { -+ getRole, -+ createRole, -+ updateRole, -+ deleteRole, -+ searchRoles -+} -diff --git a/src/services/TeamService.js b/src/services/TeamService.js -index 3f6dbfd..4052e94 100644 ---- a/src/services/TeamService.js -+++ b/src/services/TeamService.js -@@ -2,16 +2,16 @@ - * This service provides operations of Job. - */ - --const _ = require('lodash'); --const Joi = require('joi'); --const dateFNS = require('date-fns'); --const config = require('config'); --const emailTemplateConfig = require('../../config/email_template.config'); --const helper = require('../common/helper'); --const logger = require('../common/logger'); --const errors = require('../common/errors'); --const JobService = require('./JobService'); --const ResourceBookingService = require('./ResourceBookingService'); -+const _ = require('lodash') -+const Joi = require('joi') -+const dateFNS = require('date-fns') -+const config = require('config') -+const emailTemplateConfig = require('../../config/email_template.config') -+const helper = require('../common/helper') -+const logger = require('../common/logger') -+const errors = require('../common/errors') -+const JobService = require('./JobService') -+const ResourceBookingService = require('./ResourceBookingService') - - const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { - return { -@@ -20,9 +20,9 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { - from: template.from, - recipients: template.recipients, - cc: template.cc, -- sendgridTemplateId: template.sendgridTemplateId, -- }; --}); -+ sendgridTemplateId: template.sendgridTemplateId -+ } -+}) - - /** - * Function to get placed resource bookings with specific projectIds -@@ -30,14 +30,14 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { - * @param {Array} projectIds project ids - * @returns the request result - */ --async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { -- const criteria = { status: 'placed', projectIds }; -+async function _getPlacedResourceBookingsByProjectIds (currentUser, projectIds) { -+ const criteria = { status: 'placed', projectIds } - const { result } = await ResourceBookingService.searchResourceBookings( - currentUser, - criteria, - { returnAll: true } -- ); -- return result; -+ ) -+ return result - } - - /** -@@ -46,13 +46,13 @@ async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { - * @param {Array} projectIds project ids - * @returns the request result - */ --async function _getJobsByProjectIds(currentUser, projectIds) { -+async function _getJobsByProjectIds (currentUser, projectIds) { - const { result } = await JobService.searchJobs( - currentUser, - { projectIds }, - { returnAll: true } -- ); -- return result; -+ ) -+ return result - } - - /** -@@ -61,26 +61,26 @@ async function _getJobsByProjectIds(currentUser, projectIds) { - * @param {Object} criteria the search criteria - * @returns {Object} the search result, contain total/page/perPage and result array - */ --async function searchTeams(currentUser, criteria) { -- const sort = `${criteria.sortBy} ${criteria.sortOrder}`; -+async function searchTeams (currentUser, criteria) { -+ const sort = `${criteria.sortBy} ${criteria.sortOrder}` - // Get projects from /v5/projects with searching criteria - const { - total, - page, - perPage, -- result: projects, -+ result: projects - } = await helper.getProjects(currentUser, { - page: criteria.page, - perPage: criteria.perPage, - name: criteria.name, -- sort, -- }); -+ sort -+ }) - return { - total, - page, - perPage, -- result: await getTeamDetail(currentUser, projects), -- }; -+ result: await getTeamDetail(currentUser, projects) -+ } - } - - searchTeams.schema = Joi.object() -@@ -107,13 +107,13 @@ searchTeams.schema = Joi.object() - then: Joi.forbidden().label( - 'sortOrder(with sortBy being `best match`)' - ), -- otherwise: Joi.string().valid('asc', 'desc').default('desc'), -+ otherwise: Joi.string().valid('asc', 'desc').default('desc') - }), -- name: Joi.string(), -+ name: Joi.string() - }) -- .required(), -+ .required() - }) -- .required(); -+ .required() - - /** - * Get team details -@@ -122,69 +122,69 @@ searchTeams.schema = Joi.object() - * @param {Object} isSearch the flag whether for search function - * @returns {Object} the search result - */ --async function getTeamDetail(currentUser, projects, isSearch = true) { -- const projectIds = _.map(projects, 'id'); -+async function getTeamDetail (currentUser, projects, isSearch = true) { -+ const projectIds = _.map(projects, 'id') - // Get all placed resourceBookings filtered by projectIds - const resourceBookings = await _getPlacedResourceBookingsByProjectIds( - currentUser, - projectIds -- ); -+ ) - // Get all jobs filtered by projectIds -- const jobs = await _getJobsByProjectIds(currentUser, projectIds); -+ const jobs = await _getJobsByProjectIds(currentUser, projectIds) - - // Get first week day and last week day -- const curr = new Date(); -- const firstDay = dateFNS.startOfWeek(curr); -- const lastDay = dateFNS.endOfWeek(curr); -+ const curr = new Date() -+ const firstDay = dateFNS.startOfWeek(curr) -+ const lastDay = dateFNS.endOfWeek(curr) - - logger.debug({ - component: 'TeamService', - context: 'getTeamDetail', -- message: `week started: ${firstDay}, week ended: ${lastDay}`, -- }); -+ message: `week started: ${firstDay}, week ended: ${lastDay}` -+ }) - -- const result = []; -+ const result = [] - for (const project of projects) { -- const rbs = _.filter(resourceBookings, { projectId: project.id }); -- const res = _.clone(project); -- res.weeklyCost = 0; -- res.resources = []; -+ const rbs = _.filter(resourceBookings, { projectId: project.id }) -+ const res = _.clone(project) -+ res.weeklyCost = 0 -+ res.resources = [] - - if (rbs && rbs.length > 0) { - // Get minimal start date and maximal end date -- const startDates = []; -- const endDates = []; -+ const startDates = [] -+ const endDates = [] - for (const rbsItem of rbs) { - if (rbsItem.startDate) { -- startDates.push(new Date(rbsItem.startDate)); -+ startDates.push(new Date(rbsItem.startDate)) - } - if (rbsItem.endDate) { -- endDates.push(new Date(rbsItem.endDate)); -+ endDates.push(new Date(rbsItem.endDate)) - } - } - - if (startDates && startDates.length > 0) { -- res.startDate = _.min(startDates); -+ res.startDate = _.min(startDates) - } - if (endDates && endDates.length > 0) { -- res.endDate = _.max(endDates); -+ res.endDate = _.max(endDates) - } - - // Count weekly rate - for (const item of rbs) { - // ignore any resourceBooking that has customerRate missed - if (!item.customerRate) { -- continue; -+ continue - } -- const startDate = new Date(item.startDate); -- const endDate = new Date(item.endDate); -+ const startDate = new Date(item.startDate) -+ const endDate = new Date(item.endDate) - - // normally startDate is smaller than endDate for a resourceBooking so not check if startDate < endDate - if ( - (!item.startDate || startDate < lastDay) && - (!item.endDate || endDate > firstDay) - ) { -- res.weeklyCost += item.customerRate; -+ res.weeklyCost += item.customerRate - } - } - -@@ -194,48 +194,48 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { - const resource = { - id: rb.id, - userId: user.id, -- ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']), -- }; -+ ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']) -+ } - // If call function is not search, add jobId field - if (!isSearch) { -- resource.jobId = rb.jobId; -- resource.customerRate = rb.customerRate; -- resource.startDate = rb.startDate; -- resource.endDate = rb.endDate; -+ resource.jobId = rb.jobId -+ resource.customerRate = rb.customerRate -+ resource.startDate = rb.startDate -+ resource.endDate = rb.endDate - } -- return resource; -- }); -+ return resource -+ }) - }) -- ); -+ ) - if (resourceInfos && resourceInfos.length > 0) { -- res.resources = resourceInfos; -+ res.resources = resourceInfos - -- const userHandles = _.map(resourceInfos, 'handle'); -+ const userHandles = _.map(resourceInfos, 'handle') - // Get user photo from /v5/members -- const members = await helper.getMembers(userHandles); -+ const members = await helper.getMembers(userHandles) - - for (const item of res.resources) { - const findMember = _.find(members, { -- handleLower: item.handle.toLowerCase(), -- }); -+ handleLower: item.handle.toLowerCase() -+ }) - if (findMember && findMember.photoURL) { -- item.photo_url = findMember.photoURL; -+ item.photo_url = findMember.photoURL - } - } - } - } - -- const jobsTmp = _.filter(jobs, { projectId: project.id }); -+ const jobsTmp = _.filter(jobs, { projectId: project.id }) - if (jobsTmp && jobsTmp.length > 0) { - if (isSearch) { - // Count total positions -- res.totalPositions = 0; -+ res.totalPositions = 0 - for (const item of jobsTmp) { - // only sum numPositions of jobs whose status is NOT cancelled or closed - if (['cancelled', 'closed'].includes(item.status)) { -- continue; -+ continue - } -- res.totalPositions += item.numPositions; -+ res.totalPositions += item.numPositions - } - } else { - res.jobs = _.map(jobsTmp, (job) => { -@@ -249,15 +249,15 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { - 'skills', - 'customerRate', - 'status', -- 'title', -- ]); -- }); -+ 'title' -+ ]) -+ }) - } - } -- result.push(res); -+ result.push(res) - } - -- return result; -+ return result - } - - /** -@@ -266,35 +266,35 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { - * @param {String} id the job id - * @returns {Object} the team - */ --async function getTeam(currentUser, id) { -- const project = await helper.getProjectById(currentUser, id); -- const result = await getTeamDetail(currentUser, [project], false); -- const teamDetail = result[0]; -+async function getTeam (currentUser, id) { -+ const project = await helper.getProjectById(currentUser, id) -+ const result = await getTeamDetail(currentUser, [project], false) -+ const teamDetail = result[0] - - // add job skills for result -- let jobSkills = []; -+ let jobSkills = [] - if (teamDetail && teamDetail.jobs) { - for (const job of teamDetail.jobs) { - if (job.skills) { -- const usersPromises = []; -+ const usersPromises = [] - _.map(job.skills, (skillId) => { -- usersPromises.push(helper.getSkillById(skillId)); -- }); -- jobSkills = await Promise.all(usersPromises); -- job.skills = jobSkills; -+ usersPromises.push(helper.getSkillById(skillId)) -+ }) -+ jobSkills = await Promise.all(usersPromises) -+ job.skills = jobSkills - } - } - } - -- return teamDetail; -+ return teamDetail - } - - getTeam.schema = Joi.object() - .keys({ - currentUser: Joi.object().required(), -- id: Joi.number().integer().required(), -+ id: Joi.number().integer().required() - }) -- .required(); -+ .required() - - /** - * Get team job with id -@@ -303,25 +303,25 @@ getTeam.schema = Joi.object() - * @param {String} jobId the job id - * @returns the team job - */ --async function getTeamJob(currentUser, id, jobId) { -- const project = await helper.getProjectById(currentUser, id); -- const jobs = await _getJobsByProjectIds(currentUser, [project.id]); -- const job = _.find(jobs, { id: jobId }); -+async function getTeamJob (currentUser, id, jobId) { -+ const project = await helper.getProjectById(currentUser, id) -+ const jobs = await _getJobsByProjectIds(currentUser, [project.id]) -+ const job = _.find(jobs, { id: jobId }) - - if (!job) { - throw new errors.NotFoundError( - `id: ${jobId} "Job" with Team id ${id} doesn't exist` -- ); -+ ) - } - const result = { - id: job.id, -- title: job.title, -- }; -+ title: job.title -+ } - - if (job.skills) { - result.skills = await Promise.all( - _.map(job.skills, (skillId) => helper.getSkillById(skillId)) -- ); -+ ) - } - - // If the job has candidates, the following data for each candidate would be populated: -@@ -336,12 +336,12 @@ async function getTeamJob(currentUser, id, jobId) { - _.map(_.uniq(_.map(job.candidates, 'userId')), (userId) => - helper.getUserById(userId, true) - ) -- ); -- const userMap = _.groupBy(users, 'id'); -+ ) -+ const userMap = _.groupBy(users, 'id') - - // find photo URLs for users -- const members = await helper.getMembers(_.map(users, 'handle')); -- const photoURLMap = _.groupBy(members, 'handleLower'); -+ const members = await helper.getMembers(_.map(users, 'handle')) -+ const photoURLMap = _.groupBy(members, 'handleLower') - - result.candidates = _.map(job.candidates, (candidate) => { - const candidateData = _.pick(candidate, [ -@@ -349,33 +349,33 @@ async function getTeamJob(currentUser, id, jobId) { - 'resume', - 'userId', - 'interviews', -- 'id', -- ]); -- const userData = userMap[candidate.userId][0]; -+ 'id' -+ ]) -+ const userData = userMap[candidate.userId][0] - // attach user data to the candidate - Object.assign( - candidateData, - _.pick(userData, ['handle', 'firstName', 'lastName', 'skills']) -- ); -+ ) - // attach photo URL to the candidate -- const handleLower = userData.handle.toLowerCase(); -+ const handleLower = userData.handle.toLowerCase() - if (photoURLMap[handleLower]) { -- candidateData.photo_url = photoURLMap[handleLower][0].photoURL; -+ candidateData.photo_url = photoURLMap[handleLower][0].photoURL - } -- return candidateData; -- }); -+ return candidateData -+ }) - } - -- return result; -+ return result - } - - getTeamJob.schema = Joi.object() - .keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required(), -- jobId: Joi.string().guid().required(), -+ jobId: Joi.string().guid().required() - }) -- .required(); -+ .required() - - /** - * Send email through a particular template -@@ -383,21 +383,21 @@ getTeamJob.schema = Joi.object() - * @param {Object} data the email object - * @returns {undefined} - */ --async function sendEmail(currentUser, data) { -- const template = emailTemplates[data.template]; -- const dataCC = data.cc || []; -- const templateCC = template.cc || []; -- const dataRecipients = data.recipients || []; -- const templateRecipients = template.recipients || []; -+async function sendEmail (currentUser, data) { -+ const template = emailTemplates[data.template] -+ const dataCC = data.cc || [] -+ const templateCC = template.cc || [] -+ const dataRecipients = data.recipients || [] -+ const templateRecipients = template.recipients || [] - const subjectBody = { - subject: data.subject || template.subject, -- body: data.body || template.body, -- }; -+ body: data.body || template.body -+ } - for (const key in subjectBody) { - subjectBody[key] = await helper.substituteStringByObject( - subjectBody[key], - data.data -- ); -+ ) - } - const emailData = { - // override template if coming data already have the 'from' address -@@ -407,9 +407,9 @@ async function sendEmail(currentUser, data) { - cc: _.uniq([...dataCC, ...templateCC]), - data: { ...data.data, ...subjectBody }, - sendgrid_template_id: template.sendgridTemplateId, -- version: 'v3', -- }; -- await helper.postEvent(config.EMAIL_TOPIC, emailData); -+ version: 'v3' -+ } -+ await helper.postEvent(config.EMAIL_TOPIC, emailData) - } - - sendEmail.schema = Joi.object() -@@ -423,11 +423,11 @@ sendEmail.schema = Joi.object() - data: Joi.object().required(), - from: Joi.string().email(), - recipients: Joi.array().items(Joi.string().email()).allow(null), -- cc: Joi.array().items(Joi.string().email()).allow(null), -+ cc: Joi.array().items(Joi.string().email()).allow(null) - }) -- .required(), -+ .required() - }) -- .required(); -+ .required() - - /** - * Add a member to a team as customer. -@@ -437,25 +437,25 @@ sendEmail.schema = Joi.object() - * @param {String} fields the fields to be returned - * @returns {Object} the member added - */ --async function _addMemberToProjectAsCustomer(projectId, userId, fields) { -+async function _addMemberToProjectAsCustomer (projectId, userId, fields) { - try { - const member = await helper.createProjectMember( - projectId, - { userId: userId, role: 'customer' }, - { fields } -- ); -- return member; -+ ) -+ return member - } catch (err) { -- err.message = _.get(err, 'response.body.message') || err.message; -+ err.message = _.get(err, 'response.body.message') || err.message - if (err.message && err.message.includes('User already registered')) { -- throw new Error('User is already added'); -+ throw new Error('User is already added') - } - logger.error({ - component: 'TeamService', - context: '_addMemberToProjectAsCustomer', -- message: err.message, -- }); -- throw err; -+ message: err.message -+ }) -+ throw err - } - } - -@@ -467,16 +467,16 @@ async function _addMemberToProjectAsCustomer(projectId, userId, fields) { - * @param {Object} data the object including members with handle/email to be added - * @returns {Object} the success/failed added members - */ --async function addMembers(currentUser, id, criteria, data) { -- await helper.getProjectById(currentUser, id); // check whether the user can access the project -+async function addMembers (currentUser, id, criteria, data) { -+ await helper.getProjectById(currentUser, id) // check whether the user can access the project - - const result = { - success: [], -- failed: [], -- }; -+ failed: [] -+ } - -- const handles = data.handles || []; -- const emails = data.emails || []; -+ const handles = data.handles || [] -+ const emails = data.emails || [] - - const handleMembers = await helper - .getMemberDetailsByHandles(handles) -@@ -484,9 +484,9 @@ async function addMembers(currentUser, id, criteria, data) { - _.map(members, (member) => ({ - ...member, - // populate members with lower-cased handle for case insensitive search -- handleLowerCase: member.handle.toLowerCase(), -+ handleLowerCase: member.handle.toLowerCase() - })) -- ); -+ ) - - const emailMembers = await helper - .getMemberDetailsByEmails(emails) -@@ -494,20 +494,20 @@ async function addMembers(currentUser, id, criteria, data) { - _.map(members, (member) => ({ - ...member, - // populate members with lower-cased email for case insensitive search -- emailLowerCase: member.email.toLowerCase(), -+ emailLowerCase: member.email.toLowerCase() - })) -- ); -+ ) - - await Promise.all([ - Promise.all( - handles.map((handle) => { - const memberDetails = _.find(handleMembers, { -- handleLowerCase: handle.toLowerCase(), -- }); -+ handleLowerCase: handle.toLowerCase() -+ }) - - if (!memberDetails) { -- result.failed.push({ error: "User doesn't exist", handle }); -- return; -+ result.failed.push({ error: "User doesn't exist", handle }) -+ return - } - - return _addMemberToProjectAsCustomer( -@@ -517,23 +517,23 @@ async function addMembers(currentUser, id, criteria, data) { - ) - .then((member) => { - // note, that we return `handle` in the same case it was in request -- result.success.push({ ...member, handle }); -+ result.success.push({ ...member, handle }) - }) - .catch((err) => { -- result.failed.push({ error: err.message, handle }); -- }); -+ result.failed.push({ error: err.message, handle }) -+ }) - }) - ), - - Promise.all( - emails.map((email) => { - const memberDetails = _.find(emailMembers, { -- emailLowerCase: email.toLowerCase(), -- }); -+ emailLowerCase: email.toLowerCase() -+ }) - - if (!memberDetails) { -- result.failed.push({ error: "User doesn't exist", email }); -- return; -+ result.failed.push({ error: "User doesn't exist", email }) -+ return - } - - return _addMemberToProjectAsCustomer( -@@ -543,16 +543,16 @@ async function addMembers(currentUser, id, criteria, data) { - ) - .then((member) => { - // note, that we return `email` in the same case it was in request -- result.success.push({ ...member, email }); -+ result.success.push({ ...member, email }) - }) - .catch((err) => { -- result.failed.push({ error: err.message, email }); -- }); -+ result.failed.push({ error: err.message, email }) -+ }) - }) -- ), -- ]); -+ ) -+ ]) - -- return result; -+ return result - } - - addMembers.schema = Joi.object() -@@ -561,18 +561,18 @@ addMembers.schema = Joi.object() - id: Joi.number().integer().required(), - criteria: Joi.object() - .keys({ -- fields: Joi.string(), -+ fields: Joi.string() - }) - .required(), - data: Joi.object() - .keys({ - handles: Joi.array().items(Joi.string()), -- emails: Joi.array().items(Joi.string().email()), -+ emails: Joi.array().items(Joi.string().email()) - }) - .or('handles', 'emails') -- .required(), -+ .required() - }) -- .required(); -+ .required() - - /** - * Search members in a team. -@@ -583,9 +583,9 @@ addMembers.schema = Joi.object() - * @params {Object} criteria the search criteria - * @returns {Object} the search result - */ --async function searchMembers(currentUser, id, criteria) { -- const result = await helper.listProjectMembers(currentUser, id, criteria); -- return { result }; -+async function searchMembers (currentUser, id, criteria) { -+ const result = await helper.listProjectMembers(currentUser, id, criteria) -+ return { result } - } - - searchMembers.schema = Joi.object() -@@ -595,11 +595,11 @@ searchMembers.schema = Joi.object() - criteria: Joi.object() - .keys({ - role: Joi.string(), -- fields: Joi.string(), -+ fields: Joi.string() - }) -- .required(), -+ .required() - }) -- .required(); -+ .required() - - /** - * Search member invites for a team. -@@ -610,13 +610,13 @@ searchMembers.schema = Joi.object() - * @params {Object} criteria the search criteria - * @returns {Object} the search result - */ --async function searchInvites(currentUser, id, criteria) { -+async function searchInvites (currentUser, id, criteria) { - const result = await helper.listProjectMemberInvites( - currentUser, - id, - criteria -- ); -- return { result }; -+ ) -+ return { result } - } - - searchInvites.schema = Joi.object() -@@ -625,11 +625,11 @@ searchInvites.schema = Joi.object() - id: Joi.number().integer().required(), - criteria: Joi.object() - .keys({ -- fields: Joi.string(), -+ fields: Joi.string() - }) -- .required(), -+ .required() - }) -- .required(); -+ .required() - - /** - * Remove a member from a team. -@@ -640,17 +640,17 @@ searchInvites.schema = Joi.object() - * @param {String} projectMemberId the id of the project member - * @returns {undefined} - */ --async function deleteMember(currentUser, id, projectMemberId) { -- await helper.deleteProjectMember(currentUser, id, projectMemberId); -+async function deleteMember (currentUser, id, projectMemberId) { -+ await helper.deleteProjectMember(currentUser, id, projectMemberId) - } - - deleteMember.schema = Joi.object() - .keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required(), -- projectMemberId: Joi.number().integer().required(), -+ projectMemberId: Joi.number().integer().required() - }) -- .required(); -+ .required() - - /** - * Return details about the current user. -@@ -659,31 +659,31 @@ deleteMember.schema = Joi.object() - * @params {Object} criteria the search criteria - * @returns {Object} the user data for current user - */ --async function getMe(currentUser) { -- return helper.getUserByExternalId(currentUser.userId); -+async function getMe (currentUser) { -+ return helper.getUserByExternalId(currentUser.userId) - } - - getMe.schema = Joi.object() - .keys({ -- currentUser: Joi.object().required(), -+ currentUser: Joi.object().required() - }) -- .required(); -+ .required() - - /** - * @param {Object} currentUser the user performing the operation. - * @param {Object} data project data - * @returns {Object} the created project - */ --async function createProj(currentUser, data) { -- return helper.createProject(currentUser, data); -+async function createProj (currentUser, data) { -+ return helper.createProject(currentUser, data) - } - - createProj.schema = Joi.object() - .keys({ - currentUser: Joi.object().required(), -- data: Joi.object().required(), -+ data: Joi.object().required() - }) -- .required(); -+ .required() - - module.exports = { - searchTeams, -@@ -695,5 +695,5 @@ module.exports = { - searchInvites, - deleteMember, - getMe, -- createProj, --}; -+ createProj -+} --- -2.29.1.windows.1 - From 28270acb1cbcd2dbfeaf042e82908ff3579d76ac Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Tue, 1 Jun 2021 12:12:11 +0530 Subject: [PATCH 13/23] fix: added allowNull: true for new cols --- ...2021-05-28-add-fields-to-job-and-job-candidate.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/migrations/2021-05-28-add-fields-to-job-and-job-candidate.js b/migrations/2021-05-28-add-fields-to-job-and-job-candidate.js index 472a25a8..93b427b6 100644 --- a/migrations/2021-05-28-add-fields-to-job-and-job-candidate.js +++ b/migrations/2021-05-28-add-fields-to-job-and-job-candidate.js @@ -10,22 +10,22 @@ module.exports = { const transaction = await queryInterface.sequelize.transaction() try { await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'min_salary', - { type: Sequelize.INTEGER, allowNull: false }, + { type: Sequelize.INTEGER, allowNull: true }, { transaction }) await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'max_salary', - { type: Sequelize.INTEGER, allowNull: false }, + { type: Sequelize.INTEGER, allowNull: true }, { transaction }) await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'hours_per_week', - { type: Sequelize.INTEGER, allowNull: false }, + { type: Sequelize.INTEGER, allowNull: true }, { transaction }) await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'job_location', - { type: Sequelize.STRING(255), allowNull: false }, + { type: Sequelize.STRING(255), allowNull: true }, { transaction }) await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'job_timezone', - { type: Sequelize.STRING(128), allowNull: false }, + { type: Sequelize.STRING(128), allowNull: true }, { transaction }) await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'currency', - { type: Sequelize.STRING(30), allowNull: false }, + { type: Sequelize.STRING(30), allowNull: true }, { transaction }) await queryInterface.addColumn({ tableName: 'job_candidates', schema: config.DB_SCHEMA_NAME }, 'remark', { type: Sequelize.STRING(255) }, From 99c35db0203ae4a9e8fdfff13e73b3bde54ca0c9 Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Tue, 1 Jun 2021 12:50:12 +0530 Subject: [PATCH 14/23] fix: fixed syntax --- src/services/JobService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/JobService.js b/src/services/JobService.js index a30328ca..2d06fd56 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -208,7 +208,7 @@ createJob.schema = Joi.object().keys({ hoursPerWeek: Joi.number().integer().allow(null), jobLocation: Joi.string().allow(null), jobTimezone: Joi.string().allow(null), - currency: Joi.string().allow(null) + currency: Joi.string().allow(null), roleIds: Joi.array().items(Joi.string().uuid().required()) }).required() }).required() From d1ac310e2b208780d6e677fe22e413757436d3ab Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Tue, 1 Jun 2021 12:56:51 +0530 Subject: [PATCH 15/23] fix: fixed syntax comma --- src/services/JobService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/JobService.js b/src/services/JobService.js index 2d06fd56..b310d4ab 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -325,7 +325,7 @@ fullyUpdateJob.schema = Joi.object().keys({ hoursPerWeek: Joi.number().integer().allow(null), jobLocation: Joi.string().allow(null), jobTimezone: Joi.string().allow(null), - currency: Joi.string().allow(null) + currency: Joi.string().allow(null), roleIds: Joi.array().items(Joi.string().uuid().required()).default(null) }).required() }).required() From 42c2ac35c2cee34369179a925ef1d51961e3b6a6 Mon Sep 17 00:00:00 2001 From: yoution Date: Tue, 1 Jun 2021 15:28:15 +0800 Subject: [PATCH 16/23] Role & Skills Intake - Job Description Module --- ...coder-bookings-api.postman_collection.json | 42 +++++++++++- docs/swagger.yaml | 65 +++++++++++++++++++ src/common/helper.js | 24 +++++++ src/controllers/TeamController.js | 11 ++++ src/routes/TeamRoutes.js | 8 +++ src/services/TeamService.js | 24 +++++++ 6 files changed, 172 insertions(+), 2 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 74155753..0059a126 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", + "_postman_id": "b25dc4fc-ac96-49a5-b8b0-7700c625d4d0", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -17647,6 +17647,44 @@ }, "response": [] }, + { + "name": "POST /taas-teams/getSkillsByJobDescription", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"description\": \"nodejs react c++ hello\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "getSkillsByJobDescription" + ] + } + }, + "response": [] + }, { "name": "POST /taas-teams/email - member-issue-report", "request": { @@ -27007,4 +27045,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1584f5cf..4359781a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3170,6 +3170,54 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + + /taas-teams/getSkillsByJobDescription: + post: + tags: + - Teams + description: | + Get skill list by Job Description + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TeamJobDescriptionRequestBody" + responses: + "200": + description: OK + content: + application/json: + schema: + type : array + items : { + $ref: "#/components/schemas/SkillItem" + } + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /taas-teams/email: post: tags: @@ -3282,6 +3330,15 @@ components: scheme: bearer bearerFormat: JWT schemas: + SkillItem: + properties: + tag: + type: string + type: + type: string + source: + type: string + Job: required: - id @@ -4666,6 +4723,14 @@ components: type: array items: $ref: "#/components/schemas/Skill" + TeamJobDescriptionRequestBody: + type: object + properties: + description: + type: string + description: "job description" + example: "nodejs and java" + TeamEmailRequestBody: type: object properties: diff --git a/src/common/helper.js b/src/common/helper.js index e68e5c58..64226267 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1691,6 +1691,29 @@ async function substituteStringByObject(string, object) { return string; } + +/** + * Get tags from tagging service + * @param {String} description The challenge description + * @returns {Array} array of tags + */ +async function getTags (description) { + const data = { text: description, extract_confidence: false} + const type = "emsi/internal_no_refresh" + const url = `${config.TC_API}/contest-tagging/${type}`; + const res = await request + .post(url) + .set('Accept', 'application/json') + .send(querystring.stringify(data)) + + localLogger.debug({ + context: 'getTags', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.get(res, 'body'); +} + + /** * @param {Object} currentUser the user performing the action * @param {Object} data title of project and any other info @@ -1752,6 +1775,7 @@ module.exports = { getMemberDetailsByHandles, getMemberDetailsByHandle, getMemberDetailsByEmails, + getTags, createProjectMember, listProjectMembers, listProjectMemberInvites, diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index ca4f1bca..94b831c5 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -108,6 +108,16 @@ async function getMe(req, res) { res.send(await service.getMe(req.authUser)); } + +/** + * Return skills by job description. + * @param req the request + * @param res the response + */ +async function getSkillsByJobDescription(req, res) { + res.send(await service.getSkillsByJobDescription(req.authUser, req.body)); +} + /** * * @param req the request @@ -127,5 +137,6 @@ module.exports = { searchInvites, deleteMember, getMe, + getSkillsByJobDescription, createProj, }; diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index 9bbe25c6..35292b01 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -36,6 +36,14 @@ module.exports = { scopes: [constants.Scopes.READ_TAAS_TEAM], }, }, + '/taas-teams/getSkillsByJobDescription': { + post: { + controller: 'TeamController', + method: 'getSkillsByJobDescription', + auth: 'jwt', + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, + }, '/taas-teams/:id': { get: { controller: 'TeamController', diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 3f6dbfd3..2ead4d74 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -669,6 +669,29 @@ getMe.schema = Joi.object() }) .required(); +/** + * Return skills by job description. + * + * @param {Object} currentUser the user who perform this operation. + * @params {Object} criteria the search criteria + * @returns {Object} the user data for current user + */ +async function getSkillsByJobDescription(currentUser,data) { + return helper.getTags(data.description) +} + +getSkillsByJobDescription.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + data: Joi.object() + .keys({ + description: Joi.string().required(), + }) + .required(), + }) + .required(); + + /** * @param {Object} currentUser the user performing the operation. * @param {Object} data project data @@ -695,5 +718,6 @@ module.exports = { searchInvites, deleteMember, getMe, + getSkillsByJobDescription, createProj, }; From 727d09e84c9528b2e26cf7f10cf4d123740ad9a1 Mon Sep 17 00:00:00 2001 From: urwithat Date: Tue, 1 Jun 2021 14:04:38 +0530 Subject: [PATCH 17/23] Changed the endpoint roles to taas-roles --- ...coder-bookings-api.postman_collection.json | 324 +++++++++--------- docs/swagger.yaml | 6 +- src/routes/RoleRoutes.js | 4 +- 3 files changed, 167 insertions(+), 167 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 46d0389f..6d718c51 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -17966,12 +17966,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18015,12 +18015,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18064,12 +18064,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18111,12 +18111,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18158,12 +18158,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18205,12 +18205,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18252,12 +18252,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18299,12 +18299,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18346,12 +18346,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18393,12 +18393,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18440,12 +18440,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18487,12 +18487,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18534,12 +18534,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18581,12 +18581,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18628,12 +18628,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18675,12 +18675,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18722,12 +18722,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18769,12 +18769,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18816,12 +18816,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18863,12 +18863,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18910,12 +18910,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18957,12 +18957,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -19004,12 +19004,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -19045,12 +19045,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -19082,12 +19082,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-2}}?fromDb=true", + "raw": "{{URL}}/taas-roles/{{roleId-2}}?fromDb=true", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-2}}" ], "query": [ @@ -19125,12 +19125,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-3}}", + "raw": "{{URL}}/taas-roles/{{roleId-3}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-3}}" ] } @@ -19162,12 +19162,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-1}}?fromDb=true", + "raw": "{{URL}}/taas-roles/{{roleId-1}}?fromDb=true", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ], "query": [ @@ -19205,12 +19205,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-2}}", + "raw": "{{URL}}/taas-roles/{{roleId-2}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-2}}" ] } @@ -19244,12 +19244,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-2}}", + "raw": "{{URL}}/taas-roles/{{roleId-2}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-2}}" ] } @@ -19283,12 +19283,12 @@ } ], "url": { - "raw": "{{URL}}/roles/invalid", + "raw": "{{URL}}/taas-roles/invalid", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "invalid" ] } @@ -19322,12 +19322,12 @@ } ], "url": { - "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", + "raw": "{{URL}}/taas-roles/00000000-0000-0000-0000-000000000000", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "00000000-0000-0000-0000-000000000000" ] } @@ -19359,12 +19359,12 @@ } ], "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -19395,12 +19395,12 @@ } ], "url": { - "raw": "{{URL}}/roles?skillsList=dropwizard, nginx,, machine learning , FORce.com &keyword=ops e", + "raw": "{{URL}}/taas-roles?skillsList=dropwizard, nginx,, machine learning , FORce.com &keyword=ops e", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ], "query": [ { @@ -19441,12 +19441,12 @@ } ], "url": { - "raw": "{{URL}}/roles?skillsList=dataBase, ,Photoshop&keyword=sale", + "raw": "{{URL}}/taas-roles?skillsList=dataBase, ,Photoshop&keyword=sale", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ], "query": [ { @@ -19487,12 +19487,12 @@ } ], "url": { - "raw": "{{URL}}/roles?skillsList=DOCKER,.NET&keyword=dev", + "raw": "{{URL}}/taas-roles?skillsList=DOCKER,.NET&keyword=dev", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ], "query": [ { @@ -19533,12 +19533,12 @@ } ], "url": { - "raw": "{{URL}}/roles?keyword=dev", + "raw": "{{URL}}/taas-roles?keyword=dev", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ], "query": [ { @@ -19577,12 +19577,12 @@ } ], "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -19627,12 +19627,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -19673,12 +19673,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-2}}", + "raw": "{{URL}}/taas-roles/{{roleId-2}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-2}}" ] } @@ -19719,12 +19719,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-3}}", + "raw": "{{URL}}/taas-roles/{{roleId-3}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-3}}" ] } @@ -19767,12 +19767,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -19815,12 +19815,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -19863,12 +19863,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -19911,12 +19911,12 @@ } }, "url": { - "raw": "{{URL}}/roles/invalid", + "raw": "{{URL}}/taas-roles/invalid", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "invalid" ] } @@ -19959,12 +19959,12 @@ } }, "url": { - "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", + "raw": "{{URL}}/taas-roles/00000000-0000-0000-0000-000000000000", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "00000000-0000-0000-0000-000000000000" ] } @@ -20007,12 +20007,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20055,12 +20055,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20103,12 +20103,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20151,12 +20151,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20199,12 +20199,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20247,12 +20247,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20295,12 +20295,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20343,12 +20343,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20391,12 +20391,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20439,12 +20439,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20487,12 +20487,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20540,12 +20540,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20588,12 +20588,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20636,12 +20636,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20684,12 +20684,12 @@ } }, "url": { - "raw": "{{URL}}/roles/invalid", + "raw": "{{URL}}/taas-roles/invalid", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "invalid" ] } @@ -20732,12 +20732,12 @@ } }, "url": { - "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", + "raw": "{{URL}}/taas-roles/00000000-0000-0000-0000-000000000000", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "00000000-0000-0000-0000-000000000000" ] } @@ -20778,12 +20778,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20824,12 +20824,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-2}}", + "raw": "{{URL}}/taas-roles/{{roleId-2}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-2}}" ] } @@ -20870,12 +20870,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-3}}", + "raw": "{{URL}}/taas-roles/{{roleId-3}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-3}}" ] } @@ -25532,12 +25532,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -25568,12 +25568,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -25605,12 +25605,12 @@ } ], "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -25650,12 +25650,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -25696,12 +25696,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -27995,12 +27995,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -28044,12 +28044,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -28080,12 +28080,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -28117,12 +28117,12 @@ } ], "url": { - "raw": "{{URL}}/roles?keyword=Dev", + "raw": "{{URL}}/taas-roles?keyword=Dev", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ], "query": [ { @@ -28170,12 +28170,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -28218,12 +28218,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -30509,12 +30509,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -30558,12 +30558,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -30594,12 +30594,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -30631,12 +30631,12 @@ } ], "url": { - "raw": "{{URL}}/roles?skillsList=Dropwizard, ,NGINX&keyword=Dev", + "raw": "{{URL}}/taas-roles?skillsList=Dropwizard, ,NGINX&keyword=Dev", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ], "query": [ { @@ -30688,12 +30688,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -30736,12 +30736,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 808653de..40b1d474 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3258,7 +3258,7 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /roles/new: + /taas-roles/new: post: tags: - Roles @@ -3304,7 +3304,7 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /roles: + /taas-roles: get: tags: - Roles @@ -3354,7 +3354,7 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /roles/{id}: + /taas-roles/{id}: get: tags: - Roles diff --git a/src/routes/RoleRoutes.js b/src/routes/RoleRoutes.js index 2fb6d55b..7230b593 100644 --- a/src/routes/RoleRoutes.js +++ b/src/routes/RoleRoutes.js @@ -4,7 +4,7 @@ const constants = require('../../app-constants') module.exports = { - '/roles': { + '/taas-roles': { post: { controller: 'RoleController', method: 'createRole', @@ -18,7 +18,7 @@ module.exports = { scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] } }, - '/roles/:id': { + '/taas-roles/:id': { get: { controller: 'RoleController', method: 'getRole', From 71ae9ab9a5a37af4b1d1721c140935f8ea06f396 Mon Sep 17 00:00:00 2001 From: xxcxy Date: Wed, 2 Jun 2021 19:25:48 +0800 Subject: [PATCH 18/23] Payments - Batch Endpoints --- app-constants.js | 9 +- config/default.js | 3 +- data/demo-data.json | 36 +- ...coder-bookings-api.postman_collection.json | 320 ++++++++++++++---- docs/swagger.yaml | 174 +++++++++- ...-26-work-period-payment-table-migration.js | 38 +++ src/bootstrap.js | 19 +- .../WorkPeriodPaymentController.js | 13 +- src/models/WorkPeriodPayment.js | 10 +- src/routes/WorkPeriodPaymentRoutes.js | 8 + src/services/InterviewService.js | 4 +- src/services/ResourceBookingService.js | 1 - src/services/WorkPeriodPaymentService.js | 197 ++++++++--- test/unit/ResourceBookingService.test.js | 4 + test/unit/WorkPeriodPaymentService.test.js | 23 +- test/unit/common/WorkPeriodPaymentData.js | 5 +- 16 files changed, 672 insertions(+), 192 deletions(-) create mode 100644 migrations/2021-05-26-work-period-payment-table-migration.js diff --git a/app-constants.js b/app-constants.js index 534e46de..7433499a 100644 --- a/app-constants.js +++ b/app-constants.js @@ -76,9 +76,10 @@ const ChallengeStatus = { COMPLETED: 'Completed' } -const PaymentProcessingSwitch = { - ON: 'ON', - OFF: 'OFF' +const WorkPeriodPaymentStatus = { + COMPLETED: 'completed', + CANCELLED: 'cancelled', + SCHEDULED: 'scheduled' } module.exports = { @@ -87,5 +88,5 @@ module.exports = { Scopes, Interviews, ChallengeStatus, - PaymentProcessingSwitch + WorkPeriodPaymentStatus } diff --git a/config/default.js b/config/default.js index 2b5ca7ba..9b4d6e81 100644 --- a/config/default.js +++ b/config/default.js @@ -160,7 +160,6 @@ module.exports = { ROLE_ID_SUBMITTER: process.env.ROLE_ID_SUBMITTER || '732339e7-8e30-49d7-9198-cccf9451e221', TYPE_ID_TASK: process.env.TYPE_ID_TASK || 'ecd58c69-238f-43a4-a4bb-d172719b9f31', DEFAULT_TIMELINE_TEMPLATE_ID: process.env.DEFAULT_TIMELINE_TEMPLATE_ID || '53a307ce-b4b3-4d6f-b9a1-3741a58f77e6', - DEFAULT_TRACK_ID: process.env.DEFAULT_TRACK_ID || '9b6fc876-f4d9-4ccb-9dfd-419247628825', + DEFAULT_TRACK_ID: process.env.DEFAULT_TRACK_ID || '9b6fc876-f4d9-4ccb-9dfd-419247628825' - PAYMENT_PROCESSING_SWITCH: process.env.PAYMENT_PROCESSING_SWITCH || 'OFF' } diff --git a/data/demo-data.json b/data/demo-data.json index e0733443..e7fbe36e 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -182,12 +182,12 @@ { "id": "077aa2ca-5b60-4ad9-a965-1b37e08a5046", "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", - "googleCalendarId": null, + "calendarEventId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Completed", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, @@ -211,12 +211,12 @@ { "id": "b1f7ba76-640f-47e2-9463-59e51b51ec60", "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": "dummyId", + "calendarEventId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Scheduling", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, @@ -226,12 +226,12 @@ { "id": "3144fa65-ea1a-4bec-81b0-7cb1c8845826", "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": null, + "calendarEventId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Completed", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, @@ -255,12 +255,12 @@ { "id": "976d23a9-5710-453f-99d9-f57a588bb610", "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", + "calendarEventId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 3, "startTimestamp": null, - "attendeesList": [ + "guestEmails": [ "attendee1@yopmail.com", "attendee2@yopmail.com" ], @@ -273,12 +273,12 @@ { "id": "a23e1bf2-1084-4cfe-a0d8-d83bc6fec655", "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", + "calendarEventId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, - "attendeesList": [ + "guestEmails": [ "attendee1@yopmail.com", "attendee2@yopmail.com" ], @@ -291,12 +291,12 @@ { "id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb", "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": null, + "calendarEventId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Completed", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index a0518c50..4c1e6fca 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", + "_postman_id": "2252a54a-8d60-4855-acd1-51138f7edc70", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -14749,6 +14749,55 @@ }, "response": [] }, + { + "name": "create work period2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + " if(pm.response.status === \"OK\"){\r", + " const response = pm.response.json()\r", + " pm.environment.set(\"workPeriodId2\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-14\",\r\n \"endDate\": \"2021-03-20\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-periods", + "host": [ + "{{URL}}" + ], + "path": [ + "work-periods" + ] + } + }, + "response": [] + }, { "name": "create work period with m2m", "event": [ @@ -14830,7 +14879,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -14850,7 +14899,7 @@ "response": [] }, { - "name": "create work period payment with m2m create", + "name": "create multiple work period payments with boooking manager", "event": [ { "listen": "test", @@ -14858,10 +14907,6 @@ "exec": [ "pm.test('Status code is 200', function () {\r", " pm.response.to.have.status(200);\r", - " if(pm.response.status === \"OK\"){\r", - " const response = pm.response.json()\r", - " pm.environment.set(\"workPeriodPaymentIdCreatedByM2M\", response.id);\r", - " }\r", "});" ], "type": "text/javascript" @@ -14874,12 +14919,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_m2m_create_work_period_payment}}" + "value": "Bearer {{token_bookingManager}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodIdCreatedByM2M}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "[{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600\r\n},{\r\n \"workPeriodId\": \"{{workPeriodId2}}\",\r\n \"amount\": 900\r\n}]", "options": { "raw": { "language": "json" @@ -14899,16 +14944,14 @@ "response": [] }, { - "name": "create work period payment with connect user", + "name": "create query work period payments with boooking manager", "event": [ { "listen": "test", "script": { "exec": [ - "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 are not allowed to perform this action!\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -14921,12 +14964,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_connectUser}}" + "value": "Bearer {{token_bookingManager}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\"query\": { \"workPeriods.paymentStatus\": \"pending\" } }", "options": { "raw": { "language": "json" @@ -14934,28 +14977,31 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments", + "raw": "{{URL}}/work-period-payments/query", "host": [ "{{URL}}" ], "path": [ - "work-period-payments" + "work-period-payments", + "query" ] } }, "response": [] }, { - "name": "create work period payment with member", + "name": "create work period payment with m2m create", "event": [ { "listen": "test", "script": { "exec": [ - "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 are not allowed to perform this action!\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + " if(pm.response.status === \"OK\"){\r", + " const response = pm.response.json()\r", + " pm.environment.set(\"workPeriodPaymentIdCreatedByM2M\", response.id);\r", + " }\r", "});" ], "type": "text/javascript" @@ -14968,12 +15014,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_member}}" + "value": "Bearer {{token_m2m_create_work_period_payment}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodIdCreatedByM2M}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -14993,16 +15039,16 @@ "response": [] }, { - "name": "create work period payment with user id not exist", + "name": "create work period payment with connect user", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 400', function () {\r", - " pm.response.to.have.status(400);\r", + "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(\"Bad Request\")\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", "});" ], "type": "text/javascript" @@ -15015,12 +15061,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_userId_not_exist}}" + "value": "Bearer {{token_connectUser}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -15040,16 +15086,16 @@ "response": [] }, { - "name": "create work period payment with invalid token", + "name": "create work period payment with member", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 401', function () {\r", - " pm.response.to.have.status(401);\r", + "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(\"Invalid Token.\")\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", "});" ], "type": "text/javascript" @@ -15062,12 +15108,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer invalid_token" + "value": "Bearer {{token_member}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -15087,7 +15133,7 @@ "response": [] }, { - "name": "create work period payment with missing workPeriodId", + "name": "create work period payment with user id not exist", "event": [ { "listen": "test", @@ -15096,7 +15142,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(\"\\\"workPeriodPayment.workPeriodId\\\" is required\")\r", + " pm.expect(response.message).to.eq(\"Bad Request\")\r", "});" ], "type": "text/javascript" @@ -15109,12 +15155,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_userId_not_exist}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -15134,16 +15180,16 @@ "response": [] }, { - "name": "create work period payment with invalid workPeriodId 1", + "name": "create work period payment with invalid token", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 400', function () {\r", - " pm.response.to.have.status(400);\r", + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.workPeriodId\\\" must be a valid GUID\")\r", + " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", "});" ], "type": "text/javascript" @@ -15156,12 +15202,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer invalid_token" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"aaa-bb-c\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -15181,7 +15227,7 @@ "response": [] }, { - "name": "create work period payment with invalid workPeriodId 2", + "name": "create work period payment with missing workPeriodId", "event": [ { "listen": "test", @@ -15190,7 +15236,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(\"\\\"workPeriodPayment.workPeriodId\\\" must be a string\")\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.workPeriodId\\\" is required\")\r", "});" ], "type": "text/javascript" @@ -15208,7 +15254,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": 123,\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -15228,7 +15274,7 @@ "response": [] }, { - "name": "create work period payment with invalid amount 1", + "name": "create work period payment with invalid workPeriodId 1", "event": [ { "listen": "test", @@ -15237,7 +15283,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(\"\\\"workPeriodPayment.amount\\\" must be a number\")\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.workPeriodId\\\" must be a valid GUID\")\r", "});" ], "type": "text/javascript" @@ -15255,7 +15301,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": \"abc\",\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"aaa-bb-c\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -15275,7 +15321,7 @@ "response": [] }, { - "name": "create work period payment with invalid amount 2", + "name": "create work period payment with invalid workPeriodId 2", "event": [ { "listen": "test", @@ -15284,7 +15330,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(\"\\\"workPeriodPayment.amount\\\" must be greater than 0\")\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.workPeriodId\\\" must be a string\")\r", "});" ], "type": "text/javascript" @@ -15302,7 +15348,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 0,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": 123,\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -15322,7 +15368,7 @@ "response": [] }, { - "name": "create work period payment with invalid status 1", + "name": "create work period payment with invalid amount 1", "event": [ { "listen": "test", @@ -15331,7 +15377,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(\"\\\"workPeriodPayment.status\\\" must be one of [completed, cancelled]\")\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.amount\\\" must be a number\")\r", "});" ], "type": "text/javascript" @@ -15349,7 +15395,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 1200,\r\n \"status\": 123\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": \"abc\"\r\n}", "options": { "raw": { "language": "json" @@ -15369,7 +15415,7 @@ "response": [] }, { - "name": "create work period payment with invalid status 2", + "name": "create work period payment with invalid amount 2", "event": [ { "listen": "test", @@ -15378,7 +15424,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(\"\\\"workPeriodPayment.status\\\" must be one of [completed, cancelled]\")\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.amount\\\" must be greater than 0\")\r", "});" ], "type": "text/javascript" @@ -15396,7 +15442,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 1200,\r\n \"status\": \"invalid-status\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 0\r\n}", "options": { "raw": { "language": "json" @@ -22198,7 +22244,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -22217,6 +22263,52 @@ }, "response": [] }, + { + "name": "✔ create multiple work period payment with administrator", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\"query\": { \"workPeriods.paymentStatus\": \"pending\" } }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments/query", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments", + "query" + ] + } + }, + "response": [] + }, { "name": "✔ get work period payment with administrator", "event": [ @@ -24431,7 +24523,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -24450,6 +24542,54 @@ }, "response": [] }, + { + "name": "✘ create query work period payment with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member_tester1234}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\"query\": { \"workPeriods.paymentStatus\": \"pending\" } }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments/query", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments", + "query" + ] + } + }, + "response": [] + }, { "name": "✘ get work period payment with member", "event": [ @@ -26664,7 +26804,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_connect_manager}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_connect_manager}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -26683,6 +26823,54 @@ }, "response": [] }, + { + "name": "✘ create query work period payment with connect manager", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connect_manager_pshahcopmanag2}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\"query\": { \"workPeriods.paymentStatus\": \"pending\" } }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments/query", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments", + "query" + ] + } + }, + "response": [] + }, { "name": "✘ get work period payment with connect manager", "event": [ @@ -26900,4 +27088,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a0b6064b..9382de52 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2280,14 +2280,24 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/WorkPeriodPaymentRequestBody" + oneOf: + - $ref: "#/components/schemas/WorkPeriodPaymentCreateRequestBody" + - type: array + items: + $ref: "#/components/schemas/WorkPeriodPaymentCreateRequestBody" responses: "200": description: OK content: application/json: schema: - $ref: "#/components/schemas/WorkPeriodPayment" + oneOf: + - $ref: "#/components/schemas/WorkPeriodPayment" + - type: array + items: + oneOf: + - $ref: "#/components/schemas/WorkPeriodPayment" + - $ref: "#/components/schemas/WorkPeriodPaymentCreatedError" "400": description: Bad request content: @@ -2380,7 +2390,7 @@ paths: required: false schema: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "cancelled"] description: The payment status. responses: "200": @@ -2444,6 +2454,59 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /work-period-payments/query: + post: + tags: + - WorkPeriodPayments + description: | + Create Multiple Work Period Payments for all the pages at once. + + **Authorization** Topcoder token with write Work period payment scope is allowed + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/WorkPeriodPaymentQueryCreateRequestBody" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/WorkPeriodPaymentQueryCreateResult" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /work-period-payments/{id}: get: tags: @@ -4211,7 +4274,7 @@ components: description: "The amount to be paid." status: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "cancelled"] description: "The payment status." billingAccountId: type: integer @@ -4233,6 +4296,28 @@ components: type: string format: uuid description: "The user Id who updated the work period payment last time.(Will get the user info from the token)" + WorkPeriodPaymentCreatedError: + required: + - workPeriodId + properties: + workPeriodId: + type: string + format: uuid + description: "The work period id." + amount: + type: integer + example: 2 + description: "The amount to be paid." + error: + type: object + properties: + message: + type: string + description: "The error message" + code: + type: integer + example: 429 + description: "HTTP code of error" WorkPeriodPaymentRequestBody: required: - workPeriodId @@ -4247,8 +4332,85 @@ components: description: "The amount to be paid." status: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "cancelled"] description: "The payment status." + WorkPeriodPaymentCreateRequestBody: + required: + - workPeriodId + properties: + workPeriodId: + type: string + format: uuid + description: "The work period id." + amount: + type: integer + example: 2 + description: "The amount to be paid." + WorkPeriodPaymentQueryCreateRequestBody: + properties: + status: + type: string + enum: ["placed", "in-progress", "completed"] + description: The resource booking status. + startDate: + type: string + format: date + description: The resource booking start date. + endDate: + type: string + format: date + description: The resource booking end date. + rateType: + type: string + enum: ["hourly", "daily", "weekly", "monthly"] + description: The resource booking rate type. + jobId: + type: string + format: uuid + description: The job id. + userId: + type: string + format: uuid + description: The user id. + projectId: + type: integer + description: The project id. + projectIds: + oneOf: + - type: string + description: comma separated project ids. + - type: array + items: + type: integer + workPeriods.paymentStatus: + type: string + enum: ["pending", "partially-completed", "completed", "cancelled"] + workPeriods.startDate: + type: string + format: date + pattern: '^\d{4}-\d{2}-\d{2}$' + description: The work period start date. + workPeriods.endDate: + type: string + format: date + pattern: '^\d{4}-\d{2}-\d{2}$' + description: The work period end date. + workPeriods.userHandle: + type: string + description: The user handle. + WorkPeriodPaymentQueryCreateResult: + properties: + total: + type: integer + description: The total Work Periods found. + totalSuccess: + type: integer + description: The total payments scheduled successfully. + totalError: + type: integer + description: The total payments which failed to get scheduled. + query: + $ref: "#/components/schemas/WorkPeriodPaymentQueryCreateRequestBody" WorkPeriodPaymentPatchRequestBody: properties: workPeriodId: @@ -4261,7 +4423,7 @@ components: description: "The amount to be paid." status: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "cancelled"] description: "The payment status." CheckRun: type: object diff --git a/migrations/2021-05-26-work-period-payment-table-migration.js b/migrations/2021-05-26-work-period-payment-table-migration.js new file mode 100644 index 00000000..5ee8ef70 --- /dev/null +++ b/migrations/2021-05-26-work-period-payment-table-migration.js @@ -0,0 +1,38 @@ +'use strict'; +const config = require('config') + +/** + * Migrate work_period_payments challenge_id - from not null to allow null. + * enum_work_period_payments_status from completed, cancelled to completed, canceled, scheduled. + */ +module.exports = { + up: async (queryInterface, Sequelize) => { + const table = { tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME } + await Promise.all([ + queryInterface.changeColumn(table, 'challenge_id', { type: Sequelize.UUID }), + queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'scheduled'`) + ]) + }, + + down: async (queryInterface, Sequelize) => { + const table = { tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME } + await Promise.all([ + queryInterface.changeColumn(table, 'challenge_id', { type: Sequelize.UUID, allowNull: false }), + queryInterface.sequelize.query(` + DELETE + FROM + pg_enum + WHERE + enumlabel = 'scheduled' AND + enumtypid = ( + SELECT + oid + FROM + pg_type + WHERE + typname = 'enum_work_period_payments_status' + ) + `) + ]) + } +}; diff --git a/src/bootstrap.js b/src/bootstrap.js index 2999f131..9ca05431 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -2,10 +2,8 @@ const fs = require('fs') const Joi = require('joi') const path = require('path') const _ = require('lodash') -const { Interviews } = require('../app-constants') +const { Interviews, WorkPeriodPaymentStatus } = require('../app-constants') const logger = require('./common/logger') -const constants = require('../app-constants') -const config = require('config') const allowedInterviewStatuses = _.values(Interviews.Status) const allowedXAITemplate = _.keys(Interviews.XaiTemplate) @@ -16,12 +14,12 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') -Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed') +Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed') Joi.title = () => Joi.string().max(128) Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) Joi.interviewStatus = () => Joi.string().valid(...allowedInterviewStatuses) -Joi.workPeriodPaymentStatus = () => Joi.string().valid('completed', 'cancelled') +Joi.workPeriodPaymentStatus = () => Joi.string().valid(..._.values(WorkPeriodPaymentStatus)) // Empty string is not allowed by Joi by default and must be enabled with allow(''). // See https://joi.dev/api/?v=17.3.0#string fro details why it's like this. // In many cases we would like to allow empty string to make it easier to create UI for editing data. @@ -45,14 +43,3 @@ function buildServices (dir) { } buildServices(path.join(__dirname, 'services')) - -// validate some configurable parameters for the app -const paymentProcessingSwitchSchema = Joi.string().label('PAYMENT_PROCESSING_SWITCH').valid( - ...Object.values(constants.PaymentProcessingSwitch) -) -try { - Joi.attempt(config.PAYMENT_PROCESSING_SWITCH, paymentProcessingSwitchSchema) -} catch (err) { - console.error(err.message) - process.exit(1) -} diff --git a/src/controllers/WorkPeriodPaymentController.js b/src/controllers/WorkPeriodPaymentController.js index 93f5c046..4bba2385 100644 --- a/src/controllers/WorkPeriodPaymentController.js +++ b/src/controllers/WorkPeriodPaymentController.js @@ -3,7 +3,6 @@ */ const service = require('../services/WorkPeriodPaymentService') const helper = require('../common/helper') -const config = require('config') /** * Get workPeriodPayment by id @@ -20,7 +19,7 @@ async function getWorkPeriodPayment (req, res) { * @param res the response */ async function createWorkPeriodPayment (req, res) { - res.send(await service.createWorkPeriodPayment(req.authUser, req.body, { paymentProcessingSwitch: config.PAYMENT_PROCESSING_SWITCH })) + res.send(await service.createWorkPeriodPayment(req.authUser, req.body)) } /** @@ -52,9 +51,19 @@ async function searchWorkPeriodPayments (req, res) { res.send(result.result) } +/** + * Create all query workPeriodPayments + * @param req the request + * @param res the response + */ +async function createQueryWorkPeriodPayments (req, res) { + res.send(await service.createQueryWorkPeriodPayments(req.authUser, req.body)) +} + module.exports = { getWorkPeriodPayment, createWorkPeriodPayment, + createQueryWorkPeriodPayments, partiallyUpdateWorkPeriodPayment, fullyUpdateWorkPeriodPayment, searchWorkPeriodPayments diff --git a/src/models/WorkPeriodPayment.js b/src/models/WorkPeriodPayment.js index 3683faf0..7db484ea 100644 --- a/src/models/WorkPeriodPayment.js +++ b/src/models/WorkPeriodPayment.js @@ -1,6 +1,8 @@ const { Sequelize, Model } = require('sequelize') +const _ = require('lodash') const config = require('config') const errors = require('../common/errors') +const { WorkPeriodPaymentStatus } = require('../../app-constants') module.exports = (sequelize) => { class WorkPeriodPayment extends Model { @@ -44,17 +46,13 @@ module.exports = (sequelize) => { }, challengeId: { field: 'challenge_id', - type: Sequelize.UUID, - allowNull: false + type: Sequelize.UUID }, amount: { type: Sequelize.DOUBLE }, status: { - type: Sequelize.ENUM( - 'completed', - 'cancelled' - ), + type: Sequelize.ENUM(_.values(WorkPeriodPaymentStatus)), allowNull: false }, billingAccountId: { diff --git a/src/routes/WorkPeriodPaymentRoutes.js b/src/routes/WorkPeriodPaymentRoutes.js index dcc284eb..3b6f6ba9 100644 --- a/src/routes/WorkPeriodPaymentRoutes.js +++ b/src/routes/WorkPeriodPaymentRoutes.js @@ -18,6 +18,14 @@ module.exports = { scopes: [constants.Scopes.READ_WORK_PERIOD_PAYMENT, constants.Scopes.ALL_WORK_PERIOD_PAYMENT] } }, + '/work-period-payments/query': { + post: { + controller: 'WorkPeriodPaymentController', + method: 'createQueryWorkPeriodPayments', + auth: 'jwt', + scopes: [constants.Scopes.CREATE_WORK_PERIOD_PAYMENT, constants.Scopes.ALL_WORK_PERIOD_PAYMENT] + } + }, '/work-period-payments/:id': { get: { controller: 'WorkPeriodPaymentController', diff --git a/src/services/InterviewService.js b/src/services/InterviewService.js index 10a065f4..a69a788c 100644 --- a/src/services/InterviewService.js +++ b/src/services/InterviewService.js @@ -241,8 +241,8 @@ async function requestInterview (currentUser, jobCandidateId, interview) { const guestMembers = await helper.getMemberDetailsByEmails(interview.guestEmails) interview.hostName = `${hostMembers[0].firstName} ${hostMembers[0].lastName}` interview.guestNames = _.map(interview.guestEmails, (guestEmail) => { - var foundGuestMember = _.find(guestMembers, function(guestMember) { return guestEmail == guestMember.email }); - return (foundGuestMember != undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split("@")[0] + var foundGuestMember = _.find(guestMembers, function (guestMember) { return guestEmail === guestMember.email }) + return (foundGuestMember !== undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split('@')[0] }) try { diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index f5c40206..f1758c89 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -472,7 +472,6 @@ async function searchResourceBookings (currentUser, criteria, options = { return criteria.sortOrder = 'desc' } try { - throw new Error('fallback to DB') const esQuery = { index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), _source_includes: queryOpt.include, diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index c196f88e..59f63720 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -3,18 +3,17 @@ */ const _ = require('lodash') -const Joi = require('joi') +const Joi = require('joi').extend(require('@joi/date')) 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') -const constants = require('../../app-constants') const models = require('../models') -const PaymentService = require('./PaymentService') +const { WorkPeriodPaymentStatus } = require('../../app-constants') +const { searchResourceBookings } = require('./ResourceBookingService') const WorkPeriodPayment = models.WorkPeriodPayment const esClient = helper.getESClient() @@ -32,6 +31,75 @@ async function _checkUserPermissionForCRUWorkPeriodPayment (currentUser) { } } +/** + * Create single workPeriodPayment + * @param {Object} workPeriodPayment the workPeriodPayment to be created + * @param {String} createdBy the authUser id + * @returns {Object} the created workPeriodPayment + */ +async function _createSingleWorkPeriodPayment (workPeriodPayment, createdBy) { + const correspondingWorkPeriod = await helper.ensureWorkPeriodById(workPeriodPayment.workPeriodId) // ensure work period exists + + // get billingAccountId from corresponding resource booking + const correspondingResourceBooking = await helper.ensureResourceBookingById(correspondingWorkPeriod.resourceBookingId) + + return _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking(workPeriodPayment, createdBy, correspondingWorkPeriod, correspondingResourceBooking) +} + +/** + * Create single workPeriodPayment + * @param {Object} workPeriodPayment the workPeriodPayment to be created + * @param {String} createdBy the authUser id + * @param {Object} correspondingWorkPeriod the workPeriod + * @param {Object} correspondingResourceBooking the resourceBooking + * @returns {Object} the created workPeriodPayment + */ +async function _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking (workPeriodPayment, createdBy, correspondingWorkPeriod, correspondingResourceBooking) { + if (!correspondingResourceBooking.billingAccountId) { + throw new errors.ConflictError(`id: ${correspondingWorkPeriod.resourceBookingId} "ResourceBooking" Billing account is not assigned to the resource booking`) + } + workPeriodPayment.billingAccountId = correspondingResourceBooking.billingAccountId + workPeriodPayment.id = uuid.v4() + workPeriodPayment.status = WorkPeriodPaymentStatus.SCHEDULED + workPeriodPayment.createdBy = createdBy + + // set workPeriodPayment amount + if (_.isNil(workPeriodPayment.amount)) { + const memberRate = correspondingWorkPeriod.memberRate || correspondingResourceBooking.memberRate + if (_.isNil(memberRate)) { + throw new errors.BadRequestError(`Can't find a member rate in work period: ${workPeriodPayment.workPeriodId} to calculate the amount`) + } + let daysWorked = 0 + if (correspondingWorkPeriod.daysWorked) { + daysWorked = correspondingWorkPeriod.daysWorked + } else { + const matchDW = _.find(helper.extractWorkPeriods(correspondingResourceBooking.startDate, correspondingResourceBooking.endDate), { startDate: correspondingWorkPeriod.startDate }) + if (matchDW) { + daysWorked = matchDW.daysWorked + } + } + if (daysWorked === 0) { + workPeriodPayment.amount = 0 + } else { + workPeriodPayment.amount = _.round(memberRate * 5 / daysWorked, 2) + } + } + + let created = null + try { + created = await WorkPeriodPayment.create(workPeriodPayment) + } catch (err) { + if (!_.isUndefined(err.original)) { + throw new errors.BadRequestError(err.original.detail) + } else { + throw err + } + } + + await helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC, created.toJSON()) + return created.dataValues +} + /** * Get workPeriodPayment by id * @param {Object} currentUser the user who perform this operation. @@ -101,58 +169,39 @@ getWorkPeriodPayment.schema = Joi.object().keys({ * Create workPeriodPayment * @param {Object} currentUser the user who perform this operation * @param {Object} workPeriodPayment the workPeriodPayment to be created - * @param {Object} options the extra options to control the function * @returns {Object} the created workPeriodPayment */ -async function createWorkPeriodPayment (currentUser, workPeriodPayment, options = { paymentProcessingSwitch: 'OFF' }) { +async function createWorkPeriodPayment (currentUser, workPeriodPayment) { // check permission await _checkUserPermissionForCRUWorkPeriodPayment(currentUser) + const createdBy = await helper.getUserId(currentUser.userId) - const { projectId, userHandle, endDate, resourceBookingId } = await helper.ensureWorkPeriodById(workPeriodPayment.workPeriodId) // ensure work period exists - - // get billingAccountId from corresponding resource booking - const correspondingResourceBooking = await helper.ensureResourceBookingById(resourceBookingId) - if (!correspondingResourceBooking.billingAccountId) { - throw new errors.ConflictError(`id: ${resourceBookingId} "ResourceBooking" Billing account is not assigned to the resource booking`) - } - workPeriodPayment.billingAccountId = correspondingResourceBooking.billingAccountId - - const paymentChallenge = options.paymentProcessingSwitch === constants.PaymentProcessingSwitch.ON ? (await PaymentService.createPayment({ - projectId, - userHandle, - amount: workPeriodPayment.amount, - name: `TaaS Payment - ${userHandle} - Week Ending ${moment(endDate).format('D/M/YYYY')}`, - description: `TaaS Payment - ${userHandle} - Week Ending ${moment(endDate).format('D/M/YYYY')}`, - billingAccountId: correspondingResourceBooking.billingAccountId - })) : ({ id: '00000000-0000-0000-0000-000000000000' }) - - workPeriodPayment.id = uuid.v4() - workPeriodPayment.challengeId = paymentChallenge.id - workPeriodPayment.createdBy = await helper.getUserId(currentUser.userId) - - let created = null - try { - created = await WorkPeriodPayment.create(workPeriodPayment) - } catch (err) { - if (!_.isUndefined(err.original)) { - throw new errors.BadRequestError(err.original.detail) - } else { - throw err + if (_.isArray(workPeriodPayment)) { + const result = [] + for (const wp of workPeriodPayment) { + try { + const successResult = await _createSingleWorkPeriodPayment(wp, createdBy) + result.push(successResult) + } catch (e) { + result.push(_.extend(wp, { error: { message: e.message, code: e.httpStatus } })) + } } + return result + } else { + return await _createSingleWorkPeriodPayment(workPeriodPayment, createdBy) } - - await helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC, created.toJSON()) - return created.dataValues } +const singleCreateWorkPeriodPaymentSchema = Joi.object().keys({ + workPeriodId: Joi.string().uuid().required(), + amount: Joi.number().greater(0).allow(null) +}) createWorkPeriodPayment.schema = Joi.object().keys({ currentUser: Joi.object().required(), - workPeriodPayment: Joi.object().keys({ - workPeriodId: Joi.string().uuid().required(), - amount: Joi.number().greater(0).allow(null), - status: Joi.workPeriodPaymentStatus().default('completed') - }).required(), - options: Joi.object() + workPeriodPayment: Joi.alternatives().try( + singleCreateWorkPeriodPaymentSchema.required(), + Joi.array().min(1).items(singleCreateWorkPeriodPaymentSchema).required() + ).required() }).required() /** @@ -358,9 +407,67 @@ searchWorkPeriodPayments.schema = Joi.object().keys({ options: Joi.object() }).required() +/** + * Create all query workPeriodPayments + * @param {Object} currentUser the user who perform this operation. + * @param {Object} criteria the query criteria + * @returns {Object} the process result + */ +async function createQueryWorkPeriodPayments (currentUser, criteria) { + // check permission + await _checkUserPermissionForCRUWorkPeriodPayment(currentUser) + const createdBy = await helper.getUserId(currentUser.userId) + const query = criteria.query + + const fields = _.join(_.uniq(_.concat( + ['id', 'billingAccountId', 'memberRate', 'startDate', 'endDate', 'workPeriods.id', 'workPeriods.resourceBookingId', 'workPeriods.memberRate', 'workPeriods.daysWorked', 'workPeriods.startDate'], + _.map(_.keys(query), k => k === 'projectIds' ? 'projectId' : k)) + ), ',') + const searchResult = await searchResourceBookings(currentUser, _.extend({ fields, page: 1 }, query), { returnAll: 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 } + + for (const wp of wpArray) { + try { + await _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking({ workPeriodId: wp.id }, createdBy, wp, resourceBookingMap[wp.resourceBookingId]) + result.totalSuccess++ + } catch (err) { + logger.logFullError(err, { component: 'WorkPeriodPaymentService', context: 'createQueryWorkPeriodPayments' }) + result.totalError++ + } + } + return result +} + +createQueryWorkPeriodPayments.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + criteria: Joi.object().keys({ + query: Joi.object().keys({ + status: Joi.resourceBookingStatus(), + startDate: Joi.date().format('YYYY-MM-DD'), + endDate: Joi.date().format('YYYY-MM-DD'), + rateType: Joi.rateType(), + jobId: Joi.string().uuid(), + userId: Joi.string().uuid(), + projectId: Joi.number().integer(), + projectIds: Joi.alternatives( + Joi.string(), + Joi.array().items(Joi.number().integer()) + ), + 'workPeriods.paymentStatus': Joi.paymentStatus(), + 'workPeriods.startDate': Joi.date().format('YYYY-MM-DD'), + 'workPeriods.endDate': Joi.date().format('YYYY-MM-DD'), + 'workPeriods.userHandle': Joi.string() + }).required() + }).required() +}).required() + module.exports = { getWorkPeriodPayment, createWorkPeriodPayment, + createQueryWorkPeriodPayments, partiallyUpdateWorkPeriodPayment, fullyUpdateWorkPeriodPayment, searchWorkPeriodPayments diff --git a/test/unit/ResourceBookingService.test.js b/test/unit/ResourceBookingService.test.js index 64de1900..862ef357 100644 --- a/test/unit/ResourceBookingService.test.js +++ b/test/unit/ResourceBookingService.test.js @@ -455,9 +455,13 @@ describe('resourceBooking service test', () => { const stubResourceBookingFindAll = sinon.stub(ResourceBooking, 'findAll').callsFake(async () => { return data.resourceBookingFindAll }) + const stubResourceBookingCount = sinon.stub(ResourceBooking, 'count').callsFake(async () => { + return data.resourceBookingFindAll.length + }) const result = await service.searchResourceBookings(commonData.userWithManagePermission, data.criteria) expect(esClientSearch.calledOnce).to.be.true expect(stubResourceBookingFindAll.calledOnce).to.be.true + expect(stubResourceBookingCount.calledOnce).to.be.true expect(result).to.deep.eq(data.result) }) it('T26:Fail to search resource booking with not allowed fields', async () => { diff --git a/test/unit/WorkPeriodPaymentService.test.js b/test/unit/WorkPeriodPaymentService.test.js index 5e90b072..ecc11186 100644 --- a/test/unit/WorkPeriodPaymentService.test.js +++ b/test/unit/WorkPeriodPaymentService.test.js @@ -5,7 +5,6 @@ const expect = require('chai').expect const sinon = require('sinon') const models = require('../../src/models') const service = require('../../src/services/WorkPeriodPaymentService') -const paymentService = require('../../src/services/PaymentService') const commonData = require('./common/CommonData') const testData = require('./common/WorkPeriodPaymentData') const helper = require('../../src/common/helper') @@ -25,32 +24,25 @@ describe('workPeriod service test', () => { let stubEnsureWorkPeriodById let stubEnsureResourceBookingById let stubCreateWorkPeriodPayment - let stubCreatePayment beforeEach(async () => { stubGetUserId = sinon.stub(helper, 'getUserId').callsFake(async () => testData.workPeriodPayment01.getUserIdResponse) stubEnsureWorkPeriodById = sinon.stub(helper, 'ensureWorkPeriodById').callsFake(async () => testData.workPeriodPayment01.ensureWorkPeriodByIdResponse) stubEnsureResourceBookingById = sinon.stub(helper, 'ensureResourceBookingById').callsFake(async () => testData.workPeriodPayment01.ensureResourceBookingByIdResponse) stubCreateWorkPeriodPayment = sinon.stub(models.WorkPeriodPayment, 'create').callsFake(() => testData.workPeriodPayment01.response) - stubCreatePayment = sinon.stub(paymentService, 'createPayment').callsFake(async () => testData.workPeriodPayment01.createPaymentResponse) }) it('create work period success', async () => { - const response = await service.createWorkPeriodPayment(commonData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'ON' }) + const response = await service.createWorkPeriodPayment(commonData.currentUser, testData.workPeriodPayment01.request) expect(stubGetUserId.calledOnce).to.be.true expect(stubEnsureWorkPeriodById.calledOnce).to.be.true expect(stubEnsureResourceBookingById.calledOnce).to.be.true - expect(stubCreatePayment.calledOnce).to.be.true expect(stubCreateWorkPeriodPayment.calledOnce).to.be.true expect(response).to.eql(testData.workPeriodPayment01.response.dataValues) }) it('create work period success - billingAccountId is set', async () => { - await service.createWorkPeriodPayment(commonData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'ON' }) - expect(stubCreatePayment.calledOnce).to.be.true - expect(stubCreatePayment.args[0][0]).to.include({ - billingAccountId: testData.workPeriodPayment01.ensureResourceBookingByIdResponse.billingAccountId - }) + await service.createWorkPeriodPayment(commonData.currentUser, testData.workPeriodPayment01.request) expect(stubCreateWorkPeriodPayment.calledOnce).to.be.true expect(stubCreateWorkPeriodPayment.args[0][0]).to.include({ billingAccountId: testData.workPeriodPayment01.ensureResourceBookingByIdResponse.billingAccountId @@ -67,16 +59,5 @@ describe('workPeriod service test', () => { expect(err.message).to.include('"ResourceBooking" Billing account is not assigned to the resource booking') } }) - - describe('when PAYMENT_PROCESSING_SWITCH is ON/OFF', async () => { - it('do not create payment if PAYMENT_PROCESSING_SWITCH is OFF', async () => { - await service.createWorkPeriodPayment(commonData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'OFF' }) - expect(stubCreatePayment.calledOnce).to.be.false - }) - it('create payment if PAYMENT_PROCESSING_SWITCH is ON', async () => { - await service.createWorkPeriodPayment(commonData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'ON' }) - expect(stubCreatePayment.calledOnce).to.be.true - }) - }) }) }) diff --git a/test/unit/common/WorkPeriodPaymentData.js b/test/unit/common/WorkPeriodPaymentData.js index d94d9280..6e321b62 100644 --- a/test/unit/common/WorkPeriodPaymentData.js +++ b/test/unit/common/WorkPeriodPaymentData.js @@ -1,14 +1,13 @@ const workPeriodPayment01 = { request: { workPeriodId: '467b4df7-ced4-41b9-9710-b83808cddaf4', - amount: 600, - status: 'completed' + amount: 600 }, response: { dataValues: { workPeriodId: '467b4df7-ced4-41b9-9710-b83808cddaf4', amount: 600, - status: 'completed', + status: 'scheduled', id: '01971e6f-0f09-4a2a-bc2e-2adac0f00622', challengeId: '00000000-0000-0000-0000-000000000000', createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', From 67c82a7b909dfdd4178f847e8f9352d3421b22dc Mon Sep 17 00:00:00 2001 From: xxcxy Date: Wed, 2 Jun 2021 20:46:22 +0800 Subject: [PATCH 19/23] Batch Payments - Part 1 - Scheduler --- README.md | 1 + app-constants.js | 9 + app.js | 4 + config/default.js | 35 +- data/demo-data.json | 36 +- docs/swagger.yaml | 22 +- ...ler-table-add-status-details-to-payment.js | 109 ++++++ package.json | 1 + scripts/demo-payment-scheduler/data.json | 103 ++++++ scripts/demo-payment-scheduler/index.js | 81 +++++ src/bootstrap.js | 6 +- src/common/helper.js | 72 ++++ .../WorkPeriodPaymentController.js | 2 +- src/models/PaymentScheduler.js | 107 ++++++ src/models/WorkPeriodPayment.js | 14 +- src/services/InterviewService.js | 4 +- src/services/PaymentSchedulerService.js | 334 ++++++++++++++++++ src/services/PaymentService.js | 34 +- src/services/ResourceBookingService.js | 1 - test/unit/ResourceBookingService.test.js | 3 + 20 files changed, 938 insertions(+), 40 deletions(-) create mode 100644 migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js create mode 100644 scripts/demo-payment-scheduler/data.json create mode 100644 scripts/demo-payment-scheduler/index.js create mode 100644 src/models/PaymentScheduler.js create mode 100644 src/services/PaymentSchedulerService.js diff --git a/README.md b/README.md index 5e3895c2..6d41ba20 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,7 @@ To be able to change and test `taas-es-processor` locally you can follow the nex | `npm run cov` | Code Coverage Report. | | `npm run migrate` | Run any migration files which haven't run yet. | | `npm run migrate:undo` | Revert most recent migration. | +| `npm run demo-payment-scheduler` | Create 1000 Work Periods Payment records in with status "scheduled" and various "amount" | ## Import and Export data diff --git a/app-constants.js b/app-constants.js index 534e46de..2b45f516 100644 --- a/app-constants.js +++ b/app-constants.js @@ -76,6 +76,14 @@ const ChallengeStatus = { COMPLETED: 'Completed' } +const WorkPeriodPaymentStatus = { + COMPLETED: 'completed', + SCHEDULED: 'scheduled', + IN_PROGRESS: 'in-progress', + FAILED: 'failed', + CANCELLED: 'cancelled' +} + const PaymentProcessingSwitch = { ON: 'ON', OFF: 'OFF' @@ -87,5 +95,6 @@ module.exports = { Scopes, Interviews, ChallengeStatus, + WorkPeriodPaymentStatus, PaymentProcessingSwitch } diff --git a/app.js b/app.js index 7f3d7d85..e6d79c69 100644 --- a/app.js +++ b/app.js @@ -13,6 +13,7 @@ const schedule = require('node-schedule') const logger = require('./src/common/logger') const eventHandlers = require('./src/eventHandlers') const interviewService = require('./src/services/InterviewService') +const { processScheduler } = require('./src/services/PaymentSchedulerService') // setup express app const app = express() @@ -97,6 +98,9 @@ const server = app.listen(app.get('port'), () => { eventHandlers.init() // schedule updateCompletedInterviews to run every hour schedule.scheduleJob('0 0 * * * *', interviewService.updateCompletedInterviews) + + // schedule payment processing + schedule.scheduleJob(config.PAYMENT_PROCESSING.CRON, processScheduler) }) if (process.env.NODE_ENV === 'test') { diff --git a/config/default.js b/config/default.js index 2b5ca7ba..59501079 100644 --- a/config/default.js +++ b/config/default.js @@ -162,5 +162,38 @@ module.exports = { DEFAULT_TIMELINE_TEMPLATE_ID: process.env.DEFAULT_TIMELINE_TEMPLATE_ID || '53a307ce-b4b3-4d6f-b9a1-3741a58f77e6', DEFAULT_TRACK_ID: process.env.DEFAULT_TRACK_ID || '9b6fc876-f4d9-4ccb-9dfd-419247628825', - PAYMENT_PROCESSING_SWITCH: process.env.PAYMENT_PROCESSING_SWITCH || 'OFF' + PAYMENT_PROCESSING: { + // switch off actual API calls in Payment Scheduler + SWITCH: process.env.PAYMENT_PROCESSING_SWITCH || 'OFF', + // the payment scheduler cron config + CRON: process.env.PAYMENT_PROCESSING_CRON || '0 */5 * * * *', + // the number of records processed by one time + BATCH_SIZE: parseInt(process.env.PAYMENT_PROCESSING_BATCH_SIZE || 50), + // in-progress expired to determine whether a record has been processed abnormally, moment duration format + IN_PROGRESS_EXPIRED: process.env.IN_PROGRESS_EXPIRED || 'PT1H', + // the number of max retry config + MAX_RETRY_COUNT: parseInt(process.env.PAYMENT_PROCESSING_MAX_RETRY_COUNT || 10), + // the time of retry base delay, unit: ms + RETRY_BASE_DELAY: parseInt(process.env.PAYMENT_PROCESSING_RETRY_BASE_DELAY || 100), + // the time of retry max delay, unit: ms + RETRY_MAX_DELAY: parseInt(process.env.PAYMENT_PROCESSING_RETRY_MAX_DELAY || 10000), + // the max time of one request, unit: ms + PER_REQUEST_MAX_TIME: parseInt(process.env.PAYMENT_PROCESSING_PER_REQUEST_MAX_TIME || 30000), + // the max time of one payment record, unit: ms + PER_PAYMENT_MAX_TIME: parseInt(process.env.PAYMENT_PROCESSING_PER_PAYMENT_MAX_TIME || 60000), + // the max records of payment of a minute + PER_MINUTE_PAYMENT_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_PAYMENT_MAX_COUNT || 12), + // the max requests of challenge of a minute + PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT || 60), + // the max requests of resource of a minute + PER_MINUTE_RESOURCE_REQUEST_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT || 20), + // the default step fix delay, unit: ms + FIX_DELAY_STEP: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500), + // the fix delay between step one and step two, unit: ms + FIX_DELAY_STEP_1_2: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_1_2 || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500), + // the fix delay between step two and step three, unit: ms + FIX_DELAY_STEP_2_3: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_2_3 || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500), + // the fix delay between step three and step four, unit: ms + FIX_DELAY_STEP_3_4: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_3_4 || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500) + } } diff --git a/data/demo-data.json b/data/demo-data.json index e0733443..e7fbe36e 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -182,12 +182,12 @@ { "id": "077aa2ca-5b60-4ad9-a965-1b37e08a5046", "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", - "googleCalendarId": null, + "calendarEventId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Completed", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, @@ -211,12 +211,12 @@ { "id": "b1f7ba76-640f-47e2-9463-59e51b51ec60", "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": "dummyId", + "calendarEventId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Scheduling", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, @@ -226,12 +226,12 @@ { "id": "3144fa65-ea1a-4bec-81b0-7cb1c8845826", "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": null, + "calendarEventId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Completed", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, @@ -255,12 +255,12 @@ { "id": "976d23a9-5710-453f-99d9-f57a588bb610", "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", + "calendarEventId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 3, "startTimestamp": null, - "attendeesList": [ + "guestEmails": [ "attendee1@yopmail.com", "attendee2@yopmail.com" ], @@ -273,12 +273,12 @@ { "id": "a23e1bf2-1084-4cfe-a0d8-d83bc6fec655", "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", + "calendarEventId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, - "attendeesList": [ + "guestEmails": [ "attendee1@yopmail.com", "attendee2@yopmail.com" ], @@ -291,12 +291,12 @@ { "id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb", "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": null, + "calendarEventId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Completed", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a0b6064b..cf34e0a4 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2380,7 +2380,7 @@ paths: required: false schema: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"] description: The payment status. responses: "200": @@ -4211,8 +4211,22 @@ components: description: "The amount to be paid." status: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"] description: "The payment status." + statusDetails: + type: object + properties: + errorMessage: + type: string + errorCode: + type: integer + retry: + type: integer + step: + type: string + challengeId: + type: string + format: uuid billingAccountId: type: integer example: 80000071 @@ -4247,7 +4261,7 @@ components: description: "The amount to be paid." status: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"] description: "The payment status." WorkPeriodPaymentPatchRequestBody: properties: @@ -4261,7 +4275,7 @@ components: description: "The amount to be paid." status: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"] description: "The payment status." CheckRun: type: object diff --git a/migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js b/migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js new file mode 100644 index 00000000..40c1596b --- /dev/null +++ b/migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js @@ -0,0 +1,109 @@ +'use strict'; + +const config = require('config') +const _ = require('lodash') + +/** + * Create `payment_schedulers` table & relations. + */ +module.exports = { + up: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.createTable('payment_schedulers', { + id: { + type: Sequelize.UUID, + primaryKey: true, + allowNull: false, + defaultValue: Sequelize.UUIDV4 + }, + challengeId: { + field: 'challenge_id', + type: Sequelize.UUID, + allowNull: false + }, + workPeriodPaymentId: { + field: 'work_period_payment_id', + type: Sequelize.UUID, + allowNull: false, + references: { + model: { + tableName: 'work_period_payments', + schema: config.DB_SCHEMA_NAME + }, + key: 'id' + } + }, + step: { + type: Sequelize.INTEGER, + allowNull: false + }, + status: { + type: Sequelize.ENUM( + 'in-progress', + 'completed', + 'failed' + ), + allowNull: false + }, + userId: { + field: 'user_id', + type: Sequelize.BIGINT + }, + userHandle: { + field: 'user_handle', + type: Sequelize.STRING, + allowNull: false + }, + createdAt: { + field: 'created_at', + type: Sequelize.DATE + }, + updatedAt: { + field: 'updated_at', + type: Sequelize.DATE + }, + deletedAt: { + field: 'deleted_at', + type: Sequelize.DATE + } + }, { schema: config.DB_SCHEMA_NAME, transaction }) + await queryInterface.addColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'status_details', + { type: Sequelize.JSONB }, + { transaction }) + await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'challenge_id', + { type: Sequelize.UUID }, + { transaction }) + await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'scheduled'`) + await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'in-progress'`) + await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'failed'`) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + }, + + down: async (queryInterface, Sequelize) => { + const table = { schema: config.DB_SCHEMA_NAME, tableName: 'payment_schedulers' } + const statusTypeName = `${table.schema}.enum_${table.tableName}_status` + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.dropTable(table, { transaction }) + // drop enum type for status column + await queryInterface.sequelize.query(`DROP TYPE ${statusTypeName}`, { transaction }) + + await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'challenge_id', + { type: Sequelize.UUID, allowNull: false }, + { transaction }) + await queryInterface.removeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'status_details', + { transaction }) + await queryInterface.sequelize.query(`DELETE FROM pg_enum WHERE enumlabel in ('scheduled', 'in-progress', 'failed') AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'enum_work_period_payments_status')`, + { transaction }) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + } +}; diff --git a/package.json b/package.json index 0fa24cca..f434c7b8 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "local:init": "npm run local:reset && npm run data:import -- --force", "local:reset": "npm run delete-index -- --force || true && npm run create-index -- --force && npm run init-db force", "cov": "nyc --reporter=html --reporter=text npm run test", + "demo-payment-scheduler": "node scripts/demo-payment-scheduler/index.js && npm run index:all -- --force", "demo-payment": "node scripts/demo-payment" }, "keywords": [], diff --git a/scripts/demo-payment-scheduler/data.json b/scripts/demo-payment-scheduler/data.json new file mode 100644 index 00000000..5023842c --- /dev/null +++ b/scripts/demo-payment-scheduler/data.json @@ -0,0 +1,103 @@ +{ + "Job":{ + "id":"43d695d4-e926-41d5-ad42-a899612b5246", + "projectId":17234, + "title":"Dummy title - at most 64 characters", + "numPositions":13, + "skills":[ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status":"in-review", + "isApplicationPageActive":false, + "createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy":"00000000-0000-0000-0000-000000000000", + "createdAt":"2021-05-09T21:21:10.394Z", + "updatedAt":"2021-05-09T21:21:14.010Z" + }, + "ResourceBooking":{ + "id":"41671764-0ded-46fd-b7de-2af5d5e4f3fc", + "projectId":17234, + "userId":"05e988b7-7d54-4c10-ada1-1a04870a88a8", + "jobId":"43d695d4-e926-41d5-ad42-a899612b5246", + "status":"placed", + "startDate":"2020-09-27", + "endDate":"2020-10-27", + "memberRate":13.23, + "customerRate":13, + "rateType":"hourly", + "billingAccountId":80000069, + "createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy":null, + "createdAt":"2021-05-09T21:25:46.728Z", + "updatedAt":"2021-05-09T21:25:46.728Z" + }, + "WorkPeriods":[ + { + "id":"4baae2cf-fd70-4ab3-9959-e826257b7e0f", + "resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc", + "userHandle":"pshah_manager", + "projectId":17234, + "startDate":"2020-09-27", + "endDate":"2020-10-03", + "daysWorked":4, + "memberRate":27.06, + "customerRate":13.13, + "paymentStatus":"partially-completed", + "createdBy":"00000000-0000-0000-0000-000000000000", + "updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt":"2021-05-09T21:25:47.813Z", + "updatedAt":"2021-05-09T21:45:32.659Z" + }, + { + "id":"9918e1b7-acbc-41ae-baa6-fdcb2386681d", + "resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc", + "userHandle":"Shuchikr", + "projectId":17234, + "startDate":"2020-10-18", + "endDate":"2020-10-24", + "daysWorked":4, + "memberRate":4.08, + "customerRate":3.89, + "paymentStatus":"cancelled", + "createdBy":"00000000-0000-0000-0000-000000000000", + "updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt":"2021-05-09T21:25:47.834Z", + "updatedAt":"2021-05-09T21:45:37.647Z" + }, + { + "id":"42e990c9-b14c-4496-9977-c3024aa90024", + "resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc", + "userHandle":"vkumars", + "projectId":17234, + "startDate":"2020-10-25", + "endDate":"2020-10-31", + "daysWorked":3, + "memberRate":15.61, + "customerRate":9.76, + "paymentStatus":"pending", + "createdBy":"00000000-0000-0000-0000-000000000000", + "updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt":"2021-05-09T21:25:47.824Z", + "updatedAt":"2021-05-09T21:45:48.727Z" + }, + { + "id":"8bf64481-ae7b-4e51-b48c-000cd90c87d1", + "resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc", + "userHandle":"chandanant", + "projectId":17234, + "startDate":"2020-10-11", + "endDate":"2020-10-17", + "daysWorked":4, + "memberRate":10.82, + "customerRate":30.71, + "paymentStatus":"pending", + "createdBy":"00000000-0000-0000-0000-000000000000", + "updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt":"2021-05-09T21:25:47.815Z", + "updatedAt":"2021-05-09T21:45:41.810Z" + } + ] + } \ No newline at end of file diff --git a/scripts/demo-payment-scheduler/index.js b/scripts/demo-payment-scheduler/index.js new file mode 100644 index 00000000..8f76a814 --- /dev/null +++ b/scripts/demo-payment-scheduler/index.js @@ -0,0 +1,81 @@ +const { v4: uuid } = require('uuid') +const config = require('config') +const _ = require('lodash') +const data = require('./data.json') +const model = require('../../src/models') +const logger = require('../../src/common/logger') + +const payments = [] +for (let i = 0; i < 1000; i++) { + payments.push({ + id: uuid(), + workPeriodId: data.WorkPeriods[_.random(3)].id, + amount: _.round(_.random(1000, true), 2), + status: 'scheduled', + billingAccountId: data.ResourceBooking.billingAccountId, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + updatedBy: null, + createdAt: `2021-05-19T21:3${i % 10}:46.507Z`, + updatedAt: '2021-05-19T21:33:46.507Z' + }) +} + +/** + * Clear old demo data + */ +async function clearData () { + const workPeriodIds = _.join(_.map(data.WorkPeriods, w => `'${w.id}'`), ',') + await model.PaymentScheduler.destroy({ + where: { + workPeriodPaymentId: { + [model.Sequelize.Op.in]: [ + model.sequelize.literal(`select id from ${config.DB_SCHEMA_NAME}.work_period_payments where work_period_id in (${workPeriodIds})`) + ] + } + }, + force: true + }) + await model.WorkPeriodPayment.destroy({ + where: { + workPeriodId: _.map(data.WorkPeriods, 'id') + }, + force: true + }) + await model.WorkPeriod.destroy({ + where: { + id: _.map(data.WorkPeriods, 'id') + }, + force: true + }) + await model.ResourceBooking.destroy({ + where: { + id: data.ResourceBooking.id + }, + force: true + }) + await model.Job.destroy({ + where: { + id: data.Job.id + }, + force: true + }) +} + +/** + * Insert payment scheduler demo data + */ +async function insertPaymentSchedulerDemoData () { + logger.info({ component: 'payment-scheduler-demo-data', context: 'insertPaymentSchedulerDemoData', message: 'Starting to remove demo data if exists' }) + await clearData() + logger.info({ component: 'payment-scheduler-demo-data', context: 'insertPaymentSchedulerDemoData', message: 'Data cleared' }) + await model.Job.create(data.Job) + logger.info({ component: 'payment-scheduler-demo-data', context: 'insertPaymentSchedulerDemoData', message: `Job ${data.Job.id} created` }) + await model.ResourceBooking.create(data.ResourceBooking) + logger.info({ component: 'payment-scheduler-demo-data', context: 'insertPaymentSchedulerDemoData', message: `ResourceBooking: ${data.ResourceBooking.id} create` }) + await model.WorkPeriod.bulkCreate(data.WorkPeriods) + logger.info({ component: 'payment-scheduler-demo-data', context: 'insertPaymentSchedulerDemoData', message: `WorkPeriods: ${_.map(data.WorkPeriods, 'id')} created` }) + await model.WorkPeriodPayment.bulkCreate(payments) + logger.info({ component: 'payment-scheduler-demo-data', context: 'insertPaymentSchedulerDemoData', message: `${payments.length} of WorkPeriodPayments scheduled` }) +} + +insertPaymentSchedulerDemoData() diff --git a/src/bootstrap.js b/src/bootstrap.js index 2999f131..ce215169 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -16,12 +16,12 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') -Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed') +Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed') Joi.title = () => Joi.string().max(128) Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) Joi.interviewStatus = () => Joi.string().valid(...allowedInterviewStatuses) -Joi.workPeriodPaymentStatus = () => Joi.string().valid('completed', 'cancelled') +Joi.workPeriodPaymentStatus = () => Joi.string().valid(..._.values(constants.WorkPeriodPaymentStatus)) // Empty string is not allowed by Joi by default and must be enabled with allow(''). // See https://joi.dev/api/?v=17.3.0#string fro details why it's like this. // In many cases we would like to allow empty string to make it easier to create UI for editing data. @@ -51,7 +51,7 @@ const paymentProcessingSwitchSchema = Joi.string().label('PAYMENT_PROCESSING_SWI ...Object.values(constants.PaymentProcessingSwitch) ) try { - Joi.attempt(config.PAYMENT_PROCESSING_SWITCH, paymentProcessingSwitchSchema) + Joi.attempt(config.PAYMENT_PROCESSING.SWITCH, paymentProcessingSwitchSchema) } catch (err) { console.error(err.message) process.exit(1) diff --git a/src/common/helper.js b/src/common/helper.js index f7bcb148..65704098 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -151,6 +151,16 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { challengeId: { type: 'keyword' }, amount: { type: 'float' }, status: { type: 'keyword' }, + statusDetails: { + type: 'nested', + properties: { + errorMessage: { type: 'text' }, + errorCode: { type: 'integer' }, + retry: { type: 'integer' }, + step: { type: 'keyword' }, + challengeId: { type: 'keyword' } + } + }, billingAccountId: { type: 'integer' }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, @@ -202,6 +212,16 @@ async function promptUser (promptQuery, cb) { }) } +/** + * Sleep for a given number of milliseconds. + * + * @param {Number} milliseconds the sleep time + * @returns {undefined} + */ +async function sleep (milliseconds) { + return new Promise((resolve) => setTimeout(resolve, milliseconds)) +} + /** * Create index in elasticsearch * @param {Object} index the index name @@ -1288,6 +1308,26 @@ async function createChallenge (data, token) { return challenge } +/** + * Get a challenge + * + * @param {Object} data challenge data + * @returns {Object} the challenge + */ +async function getChallenge (challengeId) { + const token = await getM2MToken() + const url = `${config.TC_API}/challenges/${challengeId}` + localLogger.debug({ context: 'getChallenge', message: `EndPoint: GET ${url}` }) + const { body: challenge, status: httpStatus } = await request + .get(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + localLogger.debug({ context: 'getChallenge', message: `Status Code: ${httpStatus}` }) + localLogger.debug({ context: 'getChallenge', message: `Response Body: ${JSON.stringify(challenge)}` }) + return challenge +} + /** * Update a challenge * @@ -1339,6 +1379,35 @@ async function createChallengeResource (data, token) { return resource } +/** + * + * @param {String} challengeId the challenge id + * @param {String} memberHandle the member handle + * @param {String} roleId the role id + * @returns {Object} the resource + */ +async function getChallengeResource (challengeId, memberHandle, roleId) { + const token = await getM2MToken() + const url = `${config.TC_API}/resources?challengeId=${challengeId}&memberHandle=${memberHandle}&roleId=${roleId}` + localLogger.debug({ context: 'createChallengeResource', message: `EndPoint: POST ${url}` }) + try { + const { body: resource, status: httpStatus } = await request + .get(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + localLogger.debug({ context: 'getChallengeResource', message: `Status Code: ${httpStatus}` }) + localLogger.debug({ context: 'getChallengeResource', message: `Response Body: ${JSON.stringify(resource)}` }) + return resource[0] + } catch (err) { + if (err.status === 404) { + localLogger.debug({ context: 'getChallengeResource', message: `Status Code: ${err.status}` }) + } else { + throw err + } + } +} + /** * Populates workPeriods from start and end date of resource booking * @param {Date} start start date of the resource booking @@ -1418,6 +1487,7 @@ async function substituteStringByObject (string, object) { module.exports = { getParamFromCliArgs, promptUser, + sleep, createIndex, deleteIndex, indexBulkDataToES, @@ -1462,8 +1532,10 @@ module.exports = { deleteProjectMember, getUserAttributeValue, createChallenge, + getChallenge, updateChallenge, createChallengeResource, + getChallengeResource, extractWorkPeriods, getUserByHandle, substituteStringByObject diff --git a/src/controllers/WorkPeriodPaymentController.js b/src/controllers/WorkPeriodPaymentController.js index 93f5c046..58352deb 100644 --- a/src/controllers/WorkPeriodPaymentController.js +++ b/src/controllers/WorkPeriodPaymentController.js @@ -20,7 +20,7 @@ async function getWorkPeriodPayment (req, res) { * @param res the response */ async function createWorkPeriodPayment (req, res) { - res.send(await service.createWorkPeriodPayment(req.authUser, req.body, { paymentProcessingSwitch: config.PAYMENT_PROCESSING_SWITCH })) + res.send(await service.createWorkPeriodPayment(req.authUser, req.body, { paymentProcessingSwitch: config.PAYMENT_PROCESSING.SWITCH })) } /** diff --git a/src/models/PaymentScheduler.js b/src/models/PaymentScheduler.js new file mode 100644 index 00000000..7fd171aa --- /dev/null +++ b/src/models/PaymentScheduler.js @@ -0,0 +1,107 @@ +const { Sequelize, Model } = require('sequelize') +const config = require('config') +const errors = require('../common/errors') + +module.exports = (sequelize) => { + class PaymentScheduler extends Model { + /** + * Create association between models + * @param {Object} models the database models + */ + static associate (models) { + PaymentScheduler.belongsTo(models.WorkPeriodPayment, { foreignKey: 'workPeriodPaymentId' }) + } + + /** + * Get payment scheduler by id + * @param {String} id the payment scheduler id + * @returns {PaymentScheduler} the payment scheduler instance + */ + static async findById (id) { + const paymentScheduler = await PaymentScheduler.findOne({ + where: { + id + } + }) + if (!paymentScheduler) { + throw new errors.NotFoundError(`id: ${id} "paymentScheduler" doesn't exists`) + } + return paymentScheduler + } + } + PaymentScheduler.init( + { + id: { + type: Sequelize.UUID, + primaryKey: true, + allowNull: false, + defaultValue: Sequelize.UUIDV4 + }, + challengeId: { + field: 'challenge_id', + type: Sequelize.UUID, + allowNull: false + }, + workPeriodPaymentId: { + field: 'work_period_payment_id', + type: Sequelize.UUID, + allowNull: false + }, + step: { + type: Sequelize.INTEGER, + allowNull: false + }, + status: { + type: Sequelize.ENUM( + 'in-progress', + 'completed', + 'failed' + ), + allowNull: false + }, + userId: { + field: 'user_id', + type: Sequelize.BIGINT + }, + userHandle: { + field: 'user_handle', + type: Sequelize.STRING, + allowNull: false + }, + createdAt: { + field: 'created_at', + type: Sequelize.DATE + }, + updatedAt: { + field: 'updated_at', + type: Sequelize.DATE + }, + deletedAt: { + field: 'deleted_at', + type: Sequelize.DATE + } + }, + { + schema: config.DB_SCHEMA_NAME, + sequelize, + tableName: 'payment_schedulers', + paranoid: true, + deletedAt: 'deletedAt', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + timestamps: true, + defaultScope: { + attributes: { + exclude: ['deletedAt'] + } + }, + hooks: { + afterCreate: (paymentScheduler) => { + delete paymentScheduler.dataValues.deletedAt + } + } + } + ) + + return PaymentScheduler +} diff --git a/src/models/WorkPeriodPayment.js b/src/models/WorkPeriodPayment.js index 3683faf0..349fa653 100644 --- a/src/models/WorkPeriodPayment.js +++ b/src/models/WorkPeriodPayment.js @@ -1,6 +1,8 @@ const { Sequelize, Model } = require('sequelize') +const _ = require('lodash') const config = require('config') const errors = require('../common/errors') +const { WorkPeriodPaymentStatus } = require('../../app-constants') module.exports = (sequelize) => { class WorkPeriodPayment extends Model { @@ -44,19 +46,19 @@ module.exports = (sequelize) => { }, challengeId: { field: 'challenge_id', - type: Sequelize.UUID, - allowNull: false + type: Sequelize.UUID }, amount: { type: Sequelize.DOUBLE }, status: { - type: Sequelize.ENUM( - 'completed', - 'cancelled' - ), + type: Sequelize.ENUM(..._.values(WorkPeriodPaymentStatus)), allowNull: false }, + statusDetails: { + field: 'status_details', + type: Sequelize.JSONB + }, billingAccountId: { field: 'billing_account_id', type: Sequelize.BIGINT diff --git a/src/services/InterviewService.js b/src/services/InterviewService.js index 10a065f4..a69a788c 100644 --- a/src/services/InterviewService.js +++ b/src/services/InterviewService.js @@ -241,8 +241,8 @@ async function requestInterview (currentUser, jobCandidateId, interview) { const guestMembers = await helper.getMemberDetailsByEmails(interview.guestEmails) interview.hostName = `${hostMembers[0].firstName} ${hostMembers[0].lastName}` interview.guestNames = _.map(interview.guestEmails, (guestEmail) => { - var foundGuestMember = _.find(guestMembers, function(guestMember) { return guestEmail == guestMember.email }); - return (foundGuestMember != undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split("@")[0] + var foundGuestMember = _.find(guestMembers, function (guestMember) { return guestEmail === guestMember.email }) + return (foundGuestMember !== undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split('@')[0] }) try { diff --git a/src/services/PaymentSchedulerService.js b/src/services/PaymentSchedulerService.js new file mode 100644 index 00000000..c0607eab --- /dev/null +++ b/src/services/PaymentSchedulerService.js @@ -0,0 +1,334 @@ +const _ = require('lodash') +const config = require('config') +const moment = require('moment') +const models = require('../models') +const { getV3MemberDetailsByHandle, getChallenge, getChallengeResource, sleep, postEvent } = require('../common/helper') +const logger = require('../common/logger') +const { createChallenge, addResourceToChallenge, activateChallenge, closeChallenge } = require('./PaymentService') +const { ChallengeStatus, PaymentProcessingSwitch } = require('../../app-constants') + +const WorkPeriodPayment = models.WorkPeriodPayment +const WorkPeriod = models.WorkPeriod +const PaymentScheduler = models.PaymentScheduler +const { + SWITCH, BATCH_SIZE, IN_PROGRESS_EXPIRED, MAX_RETRY_COUNT, RETRY_BASE_DELAY, RETRY_MAX_DELAY, PER_REQUEST_MAX_TIME, PER_PAYMENT_MAX_TIME, + PER_MINUTE_PAYMENT_MAX_COUNT, PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT, PER_MINUTE_RESOURCE_REQUEST_MAX_COUNT, + FIX_DELAY_STEP_1_2, FIX_DELAY_STEP_2_3, FIX_DELAY_STEP_3_4 +} = config.PAYMENT_PROCESSING +const processStatus = { + perMin: { + minute: '0:0', + paymentsProcessed: 0, + challengeRequested: 0, + resourceRequested: 0 + }, + perMinThreshold: { + paymentsProcessed: PER_MINUTE_PAYMENT_MAX_COUNT, + challengeRequested: PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT, + resourceRequested: PER_MINUTE_RESOURCE_REQUEST_MAX_COUNT + }, + paymentStartTime: 0, + requestStartTime: 0 +} +const stepEnum = ['start-process', 'create-challenge', 'assign-member', 'activate-challenge', 'get-userId', 'close-challenge'] +const processResult = { + SUCCESS: 'success', + FAIL: 'fail', + SKIP: 'skip' +} + +const localLogger = { + debug: (message, context) => logger.debug({ component: 'PaymentSchedulerService', context, message }), + error: (message, context) => logger.error({ component: 'PaymentSchedulerService', context, message }), + info: (message, context) => logger.info({ component: 'PaymentSchedulerService', context, message }) +} + +/** + * Scheduler process entrance + */ +async function processScheduler () { + // Get the oldest Work Periods Payment records in status "scheduled" and "in-progress", + // the in progress state may be caused by an abnormal shutdown, + // or it may be a normal record that is still being processed + const workPeriodPaymentList = await WorkPeriodPayment.findAll({ where: { status: ['in-progress', 'scheduled'] }, order: [['status', 'desc'], ['createdAt']], limit: BATCH_SIZE }) + localLogger.info(`start processing ${workPeriodPaymentList.length} of payments`, 'processScheduler') + const failIds = [] + const skipIds = [] + for (const workPeriodPayment of workPeriodPaymentList) { + const result = await processPayment(workPeriodPayment) + if (result === processResult.FAIL) { + failIds.push(workPeriodPayment.id) + } else if (result === processResult.SKIP) { + skipIds.push(workPeriodPayment.id) + } + } + localLogger.info(`process end. ${workPeriodPaymentList.length - failIds.length - skipIds.length} of payments processed successfully`, 'processScheduler') + if (!_.isEmpty(skipIds)) { + localLogger.info(`payments: ${_.join(skipIds, ',')} are processing by other processor`, 'processScheduler') + } + if (!_.isEmpty(failIds)) { + localLogger.error(`payments: ${_.join(failIds, ',')} are processed failed`, 'processScheduler') + } +} + +/** + * Process a record of payment + * @param {Object} workPeriodPayment the work period payment + * @returns {String} process result + */ +async function processPayment (workPeriodPayment) { + processStatus.paymentStartTime = Date.now() + let paymentScheduler + if (workPeriodPayment.status === 'in-progress') { + paymentScheduler = await PaymentScheduler.findOne({ where: { workPeriodPaymentId: workPeriodPayment.id, status: 'in-progress' } }) + + // If the in-progress record has not expired, it is considered to be being processed by other processes + if (paymentScheduler && moment(paymentScheduler.updatedAt).add(moment.duration(IN_PROGRESS_EXPIRED)).isAfter(moment())) { + localLogger.info(`workPeriodPayment: ${workPeriodPayment.id} is being processed by other processor`, 'processPayment') + return processResult.SKIP + } + } else { + const oldValue = workPeriodPayment.toJSON() + const updated = await workPeriodPayment.update({ status: 'in-progress' }) + // Update the modified status to es + await postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue }) + } + // Check whether the number of processed records per minute exceeds the specified number, if it exceeds, wait for the next minute before processing + await checkWait(stepEnum[0]) + localLogger.info(`Processing workPeriodPayment ${workPeriodPayment.id}`, 'processPayment') + + const workPeriod = await WorkPeriod.findById(workPeriodPayment.workPeriodId) + try { + if (!paymentScheduler) { + // 1. create challenge + const challengeId = await withRetry(createChallenge, [getCreateChallengeParam(workPeriod, workPeriodPayment)], validateError, stepEnum[1]) + paymentScheduler = await PaymentScheduler.create({ challengeId, step: 1, workPeriodPaymentId: workPeriodPayment.id, userHandle: workPeriod.userHandle, status: 'in-progress' }) + } else { + // If the paymentScheduler already exists, it means that this is a record caused by an abnormal shutdown + await setPaymentSchedulerStep(paymentScheduler) + } + // Start from unprocessed step, perform the process step by step + while (paymentScheduler.step < 5) { + await processStep(paymentScheduler) + } + + const oldValue = workPeriodPayment.toJSON() + // 5. update wp and save it should only update already existent Work Period Payment record with created "challengeId" and "status=completed". + const updated = await workPeriodPayment.update({ challengeId: paymentScheduler.challengeId, status: 'completed' }) + // Update the modified status to es + await postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue }) + + await paymentScheduler.update({ step: 5, userId: paymentScheduler.userId, status: 'completed' }) + + localLogger.info(`Processed workPeriodPayment ${workPeriodPayment.id} successfully`, 'processPayment') + 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 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' }) + // Update the modified status to es + await postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue }) + + if (paymentScheduler) { + await paymentScheduler.update({ step: 5, userId: paymentScheduler.userId, status: 'failed' }) + } + localLogger.error(`Processed workPeriodPayment ${workPeriodPayment.id} failed`, 'processPayment') + return processResult.FAIL + } +} + +/** + * Perform a specific step in the process + * @param {Object} paymentScheduler the payment scheduler + */ +async function processStep (paymentScheduler) { + if (paymentScheduler.step === 1) { + // 2. assign member to the challenge + await withRetry(addResourceToChallenge, [paymentScheduler.challengeId, paymentScheduler.userHandle], validateError, stepEnum[2]) + paymentScheduler.step = 2 + } else if (paymentScheduler.step === 2) { + // 3. active the challenge + await withRetry(activateChallenge, [paymentScheduler.challengeId], validateError, stepEnum[3]) + paymentScheduler.step = 3 + } else if (paymentScheduler.step === 3) { + // 4.1. get user id + const { userId } = await withRetry(getV3MemberDetailsByHandle, [paymentScheduler.userHandle], validateError, stepEnum[4]) + paymentScheduler.userId = userId + paymentScheduler.step = 4 + } else if (paymentScheduler.step === 4) { + // 4.2. close the challenge + await withRetry(closeChallenge, [paymentScheduler.challengeId, paymentScheduler.userId, paymentScheduler.userHandle], validateError, stepEnum[5]) + paymentScheduler.step = 5 + } +} + +/** + * Set the scheduler actual step + * @param {Object} paymentScheduler the scheduler object + */ +async function setPaymentSchedulerStep (paymentScheduler) { + const challenge = await getChallenge(paymentScheduler.challengeId) + if (challenge.status === ChallengeStatus.COMPLETED) { + paymentScheduler.step = 5 + } else if (challenge.status === ChallengeStatus.ACTIVE) { + paymentScheduler.step = 3 + } else { + const resource = await getChallengeResource(paymentScheduler.challengeId, paymentScheduler.userHandle, config.ROLE_ID_SUBMITTER) + if (resource) { + paymentScheduler.step = 2 + } else { + paymentScheduler.step = 1 + } + } + // The main purpose is updating the updatedAt of payment scheduler to avoid simultaneous processing + await paymentScheduler.update({ step: paymentScheduler.step }) +} + +/** + * Generate the create challenge parameter + * @param {Object} workPeriod the work period + * @param {Object} workPeriodPayment the work period payment + * @returns {Object} the create challenge parameter + */ +function getCreateChallengeParam (workPeriod, workPeriodPayment) { + return { + projectId: workPeriod.projectId, + userHandle: workPeriod.userHandle, + amount: workPeriodPayment.amount, + name: `TaaS Payment - ${workPeriod.userHandle} - Week Ending ${moment(workPeriod.endDate).format('D/M/YYYY')}`, + description: `TaaS Payment - ${workPeriod.userHandle} - Week Ending ${moment(workPeriod.endDate).format('D/M/YYYY')}`, + billingAccountId: workPeriodPayment.billingAccountId + } +} + +/** + * Before each step is processed, wait for the corresponding time + * @param {String} step the step name + * @param {Number} tryCount the try count + */ +async function checkWait (step, tryCount) { + // When calculating the retry time later, we need to subtract the time that has been waited before + let lapse = 0 + if (step === stepEnum[0]) { + lapse += await checkPerMinThreshold('paymentsProcessed') + } else if (step === stepEnum[1]) { + await checkPerMinThreshold('challengeRequested') + } else if (step === stepEnum[2]) { + // Only when tryCount = 0, it comes from the previous step, and it is necessary to wait for a fixed time + if (FIX_DELAY_STEP_1_2 > 0 && tryCount === 0) { + await sleep(FIX_DELAY_STEP_1_2) + } + lapse += await checkPerMinThreshold('resourceRequested') + } else if (step === stepEnum[3]) { + // Only when tryCount = 0, it comes from the previous step, and it is necessary to wait for a fixed time + if (FIX_DELAY_STEP_2_3 > 0 && tryCount === 0) { + await sleep(FIX_DELAY_STEP_2_3) + } + lapse += await checkPerMinThreshold('challengeRequested') + } else if (step === stepEnum[5]) { + // Only when tryCount = 0, it comes from the previous step, and it is necessary to wait for a fixed time + if (FIX_DELAY_STEP_3_4 > 0 && tryCount === 0) { + await sleep(FIX_DELAY_STEP_3_4) + } + lapse += await checkPerMinThreshold('challengeRequested') + } + + if (tryCount > 0) { + // exponential backoff and do not exceed the maximum retry delay + const retryDelay = Math.min(RETRY_BASE_DELAY * Math.pow(2, tryCount), RETRY_MAX_DELAY) + await sleep(retryDelay - lapse) + } +} + +/** + * Determine whether the number of records processed every minute exceeds the specified number, if it exceeds, wait for the next minute + * @param {String} key the min threshold key + * @returns {Number} wait time + */ +async function checkPerMinThreshold (key) { + const mt = moment() + const min = mt.format('h:m') + let waitMs = 0 + if (processStatus.perMin.minute === min) { + if (processStatus.perMin[key] >= processStatus.perMinThreshold[key]) { + waitMs = (60 - mt.seconds()) * 1000 + localLogger.info(`The number of records of ${key} processed per minute reaches ${processStatus.perMinThreshold[key]}, and it need to wait for ${60 - mt.seconds()} seconds until the next minute`) + await sleep(waitMs) + processStatus.perMin = { + minute: moment().format('h:m'), + paymentsProcessed: 0, + challengeRequested: 0, + resourceRequested: 0 + } + } + } else { + processStatus.perMin = { + minute: min, + paymentsProcessed: 0, + challengeRequested: 0, + resourceRequested: 0 + } + } + processStatus.perMin[key]++ + return waitMs +} + +/** + * Determine whether it can try again + * @param {Object} err the process error + * @returns {Boolean} + */ +function validateError (err) { + return !err.status || err.status >= 500 +} + +/** + * Execute the function, if an exception occurs, retry according to the conditions + * @param {Function} func the main function + * @param {Array} argArr the args of main function + * @param {Function} predictFunc the determine error function + * @param {String} step the step name + * @returns the result of main function + */ +async function withRetry (func, argArr, predictFunc, step) { + let tryCount = 0 + processStatus.requestStartTime = Date.now() + while (true) { + await checkWait(step, tryCount) + tryCount++ + try { + // mock code + if (SWITCH === PaymentProcessingSwitch.OFF) { + if (step === stepEnum[1]) { + return '00000000-0000-0000-0000-000000000000' + } else if (step === stepEnum[4]) { + return { userId: 100001 } + } + return + } else { + // Execute the main function + const result = await func(...argArr) + return result + } + } catch (err) { + const now = Date.now() + // The following is the case of not retrying: + // 1. The number of retries exceeds the configured number + // 2. The thrown error does not meet the retry conditions + // 3. The request execution time exceeds the configured time + // 4. The processing time of the payment record exceeds the configured time + if (tryCount > MAX_RETRY_COUNT || !predictFunc(err) || now - processStatus.requestStartTime > PER_REQUEST_MAX_TIME || now - processStatus.paymentStartTime > PER_PAYMENT_MAX_TIME) { + err.retry = tryCount + err.step = step + throw err + } + localLogger.info(`execute ${step} with error: ${err.message}, retry...`, 'withRetry') + } + } +} + +module.exports = { + processScheduler +} diff --git a/src/services/PaymentService.js b/src/services/PaymentService.js index d06ad671..93d04e41 100644 --- a/src/services/PaymentService.js +++ b/src/services/PaymentService.js @@ -39,7 +39,8 @@ async function createPayment (options) { const challengeId = await createChallenge(options, token) await addResourceToChallenge(challengeId, options.userHandle, token) await activateChallenge(challengeId, token) - const completedChallenge = await closeChallenge(challengeId, options.userHandle, token) + const { userId } = await helper.getV3MemberDetailsByHandle(options.userHandle) + const completedChallenge = await closeChallenge(challengeId, userId, options.userHandle, token) return completedChallenge } @@ -117,6 +118,13 @@ async function addResourceToChallenge (id, handle, token) { await helper.createChallengeResource(body, token) localLogger.info({ context: 'addResourceToChallenge', message: `${handle} added to challenge ${id}` }) } catch (err) { + if (err.status === 409) { + const resource = await helper.getChallengeResource(id, handle, config.ROLE_ID_SUBMITTER) + if (resource) { + localLogger.info({ context: 'addResourceToChallenge', message: `${handle} exists in challenge ${id}` }) + return + } + } localLogger.error({ context: 'addResourceToChallenge', message: `Status Code: ${err.status}` }) localLogger.error({ context: 'addResourceToChallenge', message: err.response.text }) throw err @@ -137,6 +145,13 @@ async function activateChallenge (id, token) { await helper.updateChallenge(id, body, token) localLogger.info({ context: 'activateChallenge', message: `Challenge ${id} is activated successfully.` }) } catch (err) { + if (err.status >= 500) { + const challenge = await helper.getChallenge(id) + if (_.includes([constants.ChallengeStatus.ACTIVE, constants.ChallengeStatus.COMPLETED], challenge.status)) { + localLogger.info({ context: 'activateChallenge', message: `the status of Challenge ${id} had been ${challenge.status}.` }) + return + } + } localLogger.error({ context: 'activateChallenge', message: `Status Code: ${err.status}` }) localLogger.error({ context: 'activateChallenge', message: err.response.text }) throw err @@ -146,14 +161,14 @@ async function activateChallenge (id, token) { /** * closes the topcoder challenge * @param {String} id the challenge id + * @param {String} userId the user id * @param {String} userHandle the user handle * @param {String} token m2m token * @returns {Object} the closed challenge */ -async function closeChallenge (id, userHandle, token) { +async function closeChallenge (id, userId, userHandle, token) { localLogger.info({ context: 'closeChallenge', message: `Closing challenge ${id}` }) try { - const { userId } = await helper.getV3MemberDetailsByHandle(userHandle) const body = { status: constants.ChallengeStatus.COMPLETED, winners: [{ @@ -166,6 +181,13 @@ async function closeChallenge (id, userHandle, token) { localLogger.info({ context: 'closeChallenge', message: `Challenge ${id} is closed successfully.` }) return response } catch (err) { + if (err.status >= 500) { + const challenge = await helper.getChallenge(id) + if (constants.ChallengeStatus.COMPLETED === challenge.status) { + localLogger.info({ context: 'activateChallenge', message: `the status of Challenge ${id} had been ${challenge.status}.` }) + return challenge + } + } localLogger.error({ context: 'closeChallenge', message: `Status Code: ${err.status}` }) localLogger.error({ context: 'closeChallenge', message: err.response.text }) throw err @@ -173,5 +195,9 @@ async function closeChallenge (id, userHandle, token) { } module.exports = { - createPayment + createPayment, + createChallenge, + addResourceToChallenge, + activateChallenge, + closeChallenge } diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index f5c40206..f1758c89 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -472,7 +472,6 @@ async function searchResourceBookings (currentUser, criteria, options = { return criteria.sortOrder = 'desc' } try { - throw new Error('fallback to DB') const esQuery = { index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), _source_includes: queryOpt.include, diff --git a/test/unit/ResourceBookingService.test.js b/test/unit/ResourceBookingService.test.js index 64de1900..1fcced93 100644 --- a/test/unit/ResourceBookingService.test.js +++ b/test/unit/ResourceBookingService.test.js @@ -455,6 +455,9 @@ describe('resourceBooking service test', () => { const stubResourceBookingFindAll = sinon.stub(ResourceBooking, 'findAll').callsFake(async () => { return data.resourceBookingFindAll }) + sinon.stub(ResourceBooking, 'count').callsFake(async () => { + return data.resourceBookingFindAll.length + }) const result = await service.searchResourceBookings(commonData.userWithManagePermission, data.criteria) expect(esClientSearch.calledOnce).to.be.true expect(stubResourceBookingFindAll.calledOnce).to.be.true From 86fe56f29157965351e1699e8ede73afc5389430 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 2 Jun 2021 22:04:20 +0300 Subject: [PATCH 20/23] fix: RB search case insensitive --- src/services/ResourceBookingService.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index dead5398..7be3833e 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -556,8 +556,8 @@ async function searchResourceBookings (currentUser, criteria, options = { return 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 - _.each(workPeriodFilters, (value, key) => { + // We re-apply WorkPeriod filters except userHandle because all WPs share same userHandle + _.each(_.omit(workPeriodFilters, 'workPeriods.userHandle'), (value, key) => { key = key.split('.')[1] _.each(resourceBookings, r => { r.workPeriods = _.filter(r.workPeriods, { [key]: value }) @@ -608,11 +608,16 @@ async function searchResourceBookings (currentUser, criteria, options = { return queryCriteria.include[0].attributes = { exclude: _.map(queryOpt.excludeWP, f => _.split(f, '.')[1]) } } // Apply WorkPeriod filters - _.each(_.pick(criteria, ['workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle', 'workPeriods.paymentStatus']), (value, key) => { + _.each(_.pick(criteria, ['workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.paymentStatus']), (value, key) => { key = key.split('.')[1] queryCriteria.include[0].where[Op.and].push({ [key]: value }) - queryCriteria.include[0].required = true }) + if (criteria['workPeriods.userHandle']) { + queryCriteria.include[0].where[Op.and].push({ userHandle: { [Op.iLike]: criteria['workPeriods.userHandle'] } }) + } + if (queryCriteria.include[0].where[Op.and].length > 0) { + queryCriteria.include[0].required = true + } } // Apply sorting criteria if (!queryOpt.sortByWP) { From 39bd1c0e0e6c9569afde4ffb3ae6f77115788734 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 3 Jun 2021 00:33:27 +0300 Subject: [PATCH 21/23] fix: unable to search and create --- src/common/helper.js | 43 ++++++++++++++++++++++++++++--------- src/services/RoleService.js | 16 ++++++++------ 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index a599fb65..2f01161a 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -221,9 +221,15 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { updatedBy: { type: 'keyword' } } esIndexPropertyMapping[config.get('esConfig.ES_INDEX_ROLE')] = { - name: { type: 'keyword' }, + name: { + type: 'keyword', + normalizer: 'lowercaseNormalizer' + }, description: { type: 'keyword' }, - listOfSkills: { type: 'keyword' }, + listOfSkills: { + type: 'keyword', + normalizer: 'lowercaseNormalizer' + }, rates: { properties: { global: { type: 'integer' }, @@ -1199,6 +1205,24 @@ async function getTopcoderSkills (criteria) { } } +/** + * Function to search and retrive all skills from v5/skills + * - only returns skills from Topcoder Skills Provider defined by `TOPCODER_SKILL_PROVIDER_ID` + * + * @param {Object} criteria the search criteria + * @returns the request result + */ +async function getAllTopcoderSkills (criteria) { + const skills = await getTopcoderSkills(_.assign(criteria, { page: 1, perPage: 100 })) + while (skills.page * skills.perPage <= skills.total) { + const newSkills = await getTopcoderSkills(_.assign(criteria, { page: skills.page + 1, perPage: 100 })) + skills.result = [...skills.result, ...newSkills.result] + skills.page = newSkills.page + skills.total = newSkills.total + } + return skills.result +} + /** * Function to get skill by id * @param {String} skillId the skill Id @@ -1745,16 +1769,15 @@ async function substituteStringByObject (string, object) { return string } - /** * Get tags from tagging service * @param {String} description The challenge description * @returns {Array} array of tags */ async function getTags (description) { - const data = { text: description, extract_confidence: false} - const type = "emsi/internal_no_refresh" - const url = `${config.TC_API}/contest-tagging/${type}`; + const data = { text: description, extract_confidence: false } + const type = 'emsi/internal_no_refresh' + const url = `${config.TC_API}/contest-tagging/${type}` const res = await request .post(url) .set('Accept', 'application/json') @@ -1762,12 +1785,11 @@ async function getTags (description) { localLogger.debug({ context: 'getTags', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res, 'body'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res, 'body') } - /** * @param {Object} currentUser the user performing the action * @param {Object} data title of project and any other info @@ -1819,6 +1841,7 @@ module.exports = { getMembers, getProjectById, getTopcoderSkills, + getAllTopcoderSkills, getSkillById, ensureJobById, ensureResourceBookingById, diff --git a/src/services/RoleService.js b/src/services/RoleService.js index 19006f67..bbd2c141 100644 --- a/src/services/RoleService.js +++ b/src/services/RoleService.js @@ -32,18 +32,18 @@ async function _checkUserPermissionForWriteDeleteRole (currentUser) { * @returns {undefined} */ async function _cleanAndValidateSkillNames (skills) { - // remove duplicates, leading and trailing whitespaces, remove empties and convert to lowercase. - const cleanedSkills = _.uniq(_.filter(_.map(skills, skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill))) + // remove duplicates, leading and trailing whitespaces, empties. + const cleanedSkills = _.uniq(_.filter(_.map(skills, skill => _.trim(skill)), skill => !_.isEmpty(skill))) if (cleanedSkills.length > 0) { // search skills if they are exists - const { result } = await helper.getTopcoderSkills({ name: _.join(cleanedSkills, ',') }) + const result = await helper.getAllTopcoderSkills({ name: _.join(cleanedSkills, ',') }) const skillNames = _.map(result, 'name') // find skills that not valid - const unValidSkills = _.differenceWith(cleanedSkills, skillNames, (a, b) => _.toLower(a) === _.toLower(b)) + const unValidSkills = _.differenceBy(cleanedSkills, skillNames, _.toLower) if (unValidSkills.length > 0) { throw new errors.BadRequestError(`skills: "${unValidSkills}" are not valid`) } - return cleanedSkills + return _.intersectionBy(skillNames, cleanedSkills, _.toLower) } else { return null } @@ -232,7 +232,7 @@ deleteRole.schema = Joi.object().keys({ */ async function searchRoles (currentUser, criteria) { // clean skill names and convert into an array - criteria.skillsList = _.filter(_.map(_.split(_.trim(criteria.skillsList), ','), skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill)) + criteria.skillsList = _.filter(_.map(_.split(criteria.skillsList, ','), skill => _.trim(skill)), skill => !_.isEmpty(skill)) try { const esQuery = { index: config.get('esConfig.ES_INDEX_ROLE'), @@ -274,7 +274,9 @@ async function searchRoles (currentUser, criteria) { const filter = { [Op.and]: [] } // Apply skill name filters. listOfSkills array should include all skills provided in criteria. if (criteria.skillsList) { - filter[Op.and].push({ listOfSkills: { [Op.contains]: criteria.skillsList } }) + _.each(criteria.skillsList, skill => { + filter[Op.and].push(models.Sequelize.literal(`LOWER('${skill}') in (SELECT lower(x) FROM unnest("list_of_skills"::text[]) x)`)) + }) } // Apply name filter, allow partial match and ignore case if (criteria.keyword) { From 382d708d428b1797ec17e2af16768dc34bda3399 Mon Sep 17 00:00:00 2001 From: yoution Date: Thu, 3 Jun 2021 07:22:15 +0800 Subject: [PATCH 22/23] fix: resolve postman conflict --- ...coder-bookings-api.postman_collection.json | 118 +++++++++--------- 1 file changed, 57 insertions(+), 61 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index fec4d0d6..c1233474 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "ca01f845-9ba8-473d-b005-fc200ac3cd39", + "_postman_id": "2c9dbe94-39f9-4a01-97e4-70f781fc1364", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -19819,6 +19819,44 @@ }, "response": [] }, + { + "name": "POST /taas-teams/getSkillsByJobDescription", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"description\": \"nodejs react c++ hello\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "getSkillsByJobDescription" + ] + } + }, + "response": [] + }, { "name": "GET /taas-teams/:id/members", "request": { @@ -22554,65 +22592,23 @@ ] }, { - "name": "POST /taas-teams/getSkillsByJobDescription", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_member}}" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"description\": \"nodejs react c++ hello\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", - "host": [ - "{{URL}}" - ], - "path": [ - "taas-teams", - "getSkillsByJobDescription" - ] - } - }, - "response": [] - }, - { - "name": "POST /taas-teams/email - member-issue-report", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_member}}" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"template\": \"member-issue-report\",\n \"data\": {\n \"projectName\": \"TaaS Project Name\",\n \"projectId\": 12345,\n \"userHandle\": \"pshah_manager\",\n \"reportText\": \"I have issue with ...\"\n }\n}", - "options": { - "raw": { - "language": "json" + "name": "Delete Role", + "item": [ + { + "name": "delete role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } } ], "request": { @@ -32849,4 +32845,4 @@ ] } ] -} \ No newline at end of file +} From 15a00f410dc4fe4421d03d28e4457ec8816719ea Mon Sep 17 00:00:00 2001 From: xxcxy Date: Sat, 5 Jun 2021 11:10:32 +0800 Subject: [PATCH 23/23] fix some review issues --- src/services/PaymentSchedulerService.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/services/PaymentSchedulerService.js b/src/services/PaymentSchedulerService.js index c0607eab..963f2e95 100644 --- a/src/services/PaymentSchedulerService.js +++ b/src/services/PaymentSchedulerService.js @@ -170,7 +170,9 @@ async function processStep (paymentScheduler) { */ async function setPaymentSchedulerStep (paymentScheduler) { const challenge = await getChallenge(paymentScheduler.challengeId) - if (challenge.status === ChallengeStatus.COMPLETED) { + if (SWITCH === PaymentProcessingSwitch.OFF) { + paymentScheduler.step = 5 + } else if (challenge.status === ChallengeStatus.COMPLETED) { paymentScheduler.step = 5 } else if (challenge.status === ChallengeStatus.ACTIVE) { paymentScheduler.step = 3 @@ -301,6 +303,8 @@ async function withRetry (func, argArr, predictFunc, step) { try { // mock code if (SWITCH === PaymentProcessingSwitch.OFF) { + // without actual API calls by adding delay (for example 1 second for each step), to simulate the act + sleep(1000) if (step === stepEnum[1]) { return '00000000-0000-0000-0000-000000000000' } else if (step === stepEnum[4]) { @@ -316,7 +320,7 @@ async function withRetry (func, argArr, predictFunc, step) { const now = Date.now() // The following is the case of not retrying: // 1. The number of retries exceeds the configured number - // 2. The thrown error does not meet the retry conditions + // 2. The thrown error does not match the retry conditions // 3. The request execution time exceeds the configured time // 4. The processing time of the payment record exceeds the configured time if (tryCount > MAX_RETRY_COUNT || !predictFunc(err) || now - processStatus.requestStartTime > PER_REQUEST_MAX_TIME || now - processStatus.paymentStartTime > PER_PAYMENT_MAX_TIME) {