diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 3cd72f7b..1a257ab2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1002,6 +1002,32 @@ paths: type: string enum: ['hourly', 'daily', 'weekly', 'monthly'] description: The rate type. + - in: query + name: jobId + required: false + schema: + type: string + format: uuid + description: The job id. + - in: query + name: userId + required: false + schema: + type: string + format: uuid + description: The job id. + - in: query + name: projectId + required: false + schema: + type: integer + description: The project id. + - in: query + name: projectIds + required: false + schema: + type: string + description: comma separated project ids. responses: '200': diff --git a/package-lock.json b/package-lock.json index 5b90459b..26e22f89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -414,6 +414,15 @@ "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "dev": true }, + "@joi/date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@joi/date/-/date-2.0.1.tgz", + "integrity": "sha512-vAnBxaPmyXRmHlbr5H3zY6x8ToW1a3c3gCo91dsf/HPKP2vS4sz2xzjyCE1up0vmFmSWgfDIyJMpRWVOG2cpZQ==", + "dev": true, + "requires": { + "moment": "2.x.x" + } + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -1630,6 +1639,15 @@ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, + "csv-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.0.0.tgz", + "integrity": "sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, "d": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", diff --git a/package.json b/package.json index 15788a91..61621f77 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,9 @@ "winston": "^3.3.3" }, "devDependencies": { + "@joi/date": "^2.0.1", "chai": "^4.2.0", + "csv-parser": "^3.0.0", "mocha": "^8.1.3", "nodemon": "^2.0.4", "nyc": "^15.1.0", diff --git a/scripts/recruit-crm-job-import/README.md b/scripts/recruit-crm-job-import/README.md new file mode 100644 index 00000000..fdd8e01a --- /dev/null +++ b/scripts/recruit-crm-job-import/README.md @@ -0,0 +1,76 @@ +Recruit CRM Data Import +=== + +# Configuration +Configuration file is at `./scripts/recruit-crm-job-import/config.js`. + + +# Usage +``` bash +node scripts/recruit-crm-job-import +``` + +By default the script creates jobs and resource bookings via `TC_API`. +# Example + +Follow the README for Taas API to deploy Taas API locally and then point the script to the local API by running: + +``` bash +export RCRM_IMPORT_TAAS_API_URL=http://localhost:3000/api/v5 +node scripts/recruit-crm-job-import scripts/recruit-crm-job-import/example_data.csv | tee /tmp/report.txt +``` + +The example output is: + +``` bash +DEBUG: processing line #1 - {"directProjectId":"24568","projectId":"(dynamic load)","externalId":"","title":"taas-demo-job5","startDate":"10/26/2020","endDate":"01/29/2021","numPositions":"2","userHandle":"nkumartest","jobid":"(dynamic load)","customerRate":"20","memberRate":"10","_lnum":1} +WARN: #1 - externalId is missing +DEBUG: processed line #1 +DEBUG: processing line #2 - {"directProjectId":"24568","projectId":"(dynamic load)","externalId":"0","title":"taas-demo-job5","startDate":"10/26/2020","endDate":"01/29/2021","numPositions":"2","userHandle":"not_found_handle","jobid":"(dynamic load)","customerRate":"20","memberRate":"10","_lnum":2} +ERROR: #2 - handle: not_found_handle user not found +DEBUG: processed line #2 +DEBUG: processing line #3 - {"directProjectId":"24568","projectId":"(dynamic load)","externalId":"0","title":"taas-demo-job5","startDate":"10/26/2020","endDate":"01/29/2021","numPositions":"2","userHandle":"nkumartest","jobid":"(dynamic load)","customerRate":"20","memberRate":"10","_lnum":3} +DEBUG: userHandle: nkumartest userId: 57646ff9-1cd3-4d3c-88ba-eb09a395366c +DEBUG: resourceBookingId: dc8b23d4-9987-4a7d-a587-2056283223de status: assigned +INFO: #3 - id: 7c8ed989-35bf-4899-9c93-708630a7c63b job already exists; id: dc8b23d4-9987-4a7d-a587-2056283223de resource booking created; id: dc8b23d4-9987-4a7d-a587-2056283223de status: assigned resource booking updated +DEBUG: processed line #3 +DEBUG: processing line #4 - {"directProjectId":"24567","projectId":"(dynamic load)","externalId":"1212","title":"Dummy Description","startDate":"10/20/2020","endDate":"01/29/2021","numPositions":"2","userHandle":"pshah_manager","jobid":"(dynamic load)","customerRate":"150","memberRate":"100","_lnum":4} +DEBUG: userHandle: pshah_manager userId: a55fe1bc-1754-45fa-9adc-cf3d6d7c377a +DEBUG: resourceBookingId: 708469fb-ead0-4fc3-bef7-1ef4dd041428 status: assigned +INFO: #4 - id: f61da880-5295-40c2-b6db-21e6cdef93f9 job created; id: 708469fb-ead0-4fc3-bef7-1ef4dd041428 resource booking created; id: 708469fb-ead0-4fc3-bef7-1ef4dd041428 status: assigned resource booking updated +DEBUG: processed line #4 +DEBUG: processing line #5 - {"directProjectId":"24566","projectId":"(dynamic load)","externalId":"23850272","title":"33fromzaps330","startDate":"02/21/2021","endDate":"03/15/2021","numPositions":"7","userHandle":"nkumar2","jobid":"(dynamic load)","customerRate":"50","memberRate":"30","_lnum":5} +DEBUG: userHandle: nkumar2 userId: 4b00d029-c87b-47b2-bfe2-0ab80d8b5774 +DEBUG: resourceBookingId: 7870c30b-e511-48f2-8687-499ab116174f status: assigned +INFO: #5 - id: 72dc0399-5e4b-4783-9a27-ea07a4ce99a7 job created; id: 7870c30b-e511-48f2-8687-499ab116174f resource booking created; id: 7870c30b-e511-48f2-8687-499ab116174f status: assigned resource booking updated +DEBUG: processed line #5 +DEBUG: processing line #6 - {"directProjectId":"24565","projectId":"(dynamic load)","externalId":"23843365","title":"Designer","startDate":"02/24/2021","endDate":"03/30/2021","numPositions":"1","userHandle":"GunaK-TopCoder","jobid":"(dynamic load)","customerRate":"70","memberRate":"70","_lnum":6} +DEBUG: userHandle: GunaK-TopCoder userId: 2bba34d5-20e4-46d6-bfc1-05736b17afbb +DEBUG: resourceBookingId: b2e705d3-6864-4697-96bb-dc2a288755bc status: assigned +INFO: #6 - id: 7ff0737e-958c-494e-8a5a-592ac1c5d4ff job created; id: b2e705d3-6864-4697-96bb-dc2a288755bc resource booking created; id: b2e705d3-6864-4697-96bb-dc2a288755bc status: assigned resource booking updated +DEBUG: processed line #6 +DEBUG: processing line #7 - {"directProjectId":"24564","projectId":"(dynamic load)","externalId":"23836459","title":"demo-dev-19janV4","startDate":"01/20/2021","endDate":"01/30/2021","numPositions":"1","userHandle":"nkumar1","jobid":"(dynamic load)","customerRate":"400","memberRate":"200","_lnum":7} +DEBUG: userHandle: nkumar1 userId: ab19a53b-0607-4a99-8bdd-f3b0cb552293 +DEBUG: resourceBookingId: 04299b4c-3f6e-4b3e-ae57-bf8232408cf9 status: assigned +INFO: #7 - id: 73301ade-40ff-4103-bd50-37b8d2a98183 job created; id: 04299b4c-3f6e-4b3e-ae57-bf8232408cf9 resource booking created; id: 04299b4c-3f6e-4b3e-ae57-bf8232408cf9 status: assigned resource booking updated +DEBUG: processed line #7 +INFO: === summary === +INFO: total: 7 +INFO: success: 5 +INFO: failure: 1 +INFO: skips: 1 +INFO: === summary === +INFO: done! +``` + +To list all skipped lines: + +``` bash +cat /tmp/report.txt | grep 'WARN' +``` + +To find out whether there are some users not found by user handles, run the following command: + +``` bash +cat /tmp/report.txt | grep 'ERROR' | grep 'user not found' +``` diff --git a/scripts/recruit-crm-job-import/config.js b/scripts/recruit-crm-job-import/config.js new file mode 100644 index 00000000..58a96031 --- /dev/null +++ b/scripts/recruit-crm-job-import/config.js @@ -0,0 +1,21 @@ +/* + * Configuration for the RCRM import script. + * Namespace is created to allow to configure the env variables for this script independently. + */ + +const config = require('config') + +const namespace = process.env.RCRM_IMPORT_CONFIG_NAMESAPCE || 'RCRM_IMPORT_' + +module.exports = { + SLEEP_TIME: process.env[`${namespace}SLEEP_TIME`] || 500, + TAAS_API_URL: process.env[`${namespace}TAAS_API_URL`] || config.TC_API, + + TC_API: process.env[`${namespace}TC_API`] || config.TC_API, + AUTH0_URL: process.env[`${namespace}AUTH0_URL`] || config.AUTH0_URL, + AUTH0_AUDIENCE: process.env[`${namespace}AUTH0_AUDIENCE`] || config.AUTH0_AUDIENCE, + TOKEN_CACHE_TIME: process.env[`${namespace}TOKEN_CACHE_TIME`] || config.TOKEN_CACHE_TIME, + AUTH0_CLIENT_ID: process.env[`${namespace}AUTH0_CLIENT_ID`] || config.AUTH0_CLIENT_ID, + AUTH0_CLIENT_SECRET: process.env[`${namespace}AUTH0_CLIENT_SECRET`] || config.AUTH0_CLIENT_SECRET, + AUTH0_PROXY_SERVER_URL: process.env[`${namespace}AUTH0_PROXY_SERVER_URL`] || config.AUTH0_PROXY_SERVER_URL +} diff --git a/scripts/recruit-crm-job-import/constants.js b/scripts/recruit-crm-job-import/constants.js new file mode 100644 index 00000000..7dbe4743 --- /dev/null +++ b/scripts/recruit-crm-job-import/constants.js @@ -0,0 +1,22 @@ +/* + * Constants for the RCRM import script. + */ + +module.exports = { + ProcessingStatus: { + Successful: 'successful', + Failed: 'failed', + Skipped: 'skipped' + }, + fieldNameMap: { + DirectprojectId: 'directProjectId', + externalId: 'externalId', + title: 'title', + startDate: 'startDate', + endDate: 'endDate', + numPositions: 'numPositions', + userHandle: 'userHandle', + customerRate: 'customerRate', + memberRate: 'memberRate' + } +} diff --git a/scripts/recruit-crm-job-import/example_data.csv b/scripts/recruit-crm-job-import/example_data.csv new file mode 100644 index 00000000..1a52211d --- /dev/null +++ b/scripts/recruit-crm-job-import/example_data.csv @@ -0,0 +1,8 @@ +DirectprojectId,projectId,externalId,title,startDate,endDate,numPositions,userHandle,jobid,customerRate,memberRate +24568,(dynamic load),,taas-demo-job5,10/26/2020,01/29/2021,2,nkumartest,(dynamic load),20,10 +24568,(dynamic load),0,taas-demo-job5,10/26/2020,01/29/2021,2,not_found_handle,(dynamic load),20,10 +24568,(dynamic load),0,taas-demo-job5,10/26/2020,01/29/2021,2,nkumartest,(dynamic load),20,10 +24567,(dynamic load),1212,Dummy Description,10/20/2020,01/29/2021,2,pshah_manager,(dynamic load),150,100 +24566,(dynamic load),23850272,33fromzaps330,02/21/2021,03/15/2021,7,nkumar2,(dynamic load),50,30 +24565,(dynamic load),23843365,Designer,02/24/2021,03/30/2021,1,GunaK-TopCoder,(dynamic load),70,70 +24564,(dynamic load),23836459,demo-dev-19janV4,01/20/2021,01/30/2021,1,nkumar1,(dynamic load),400,200 diff --git a/scripts/recruit-crm-job-import/helper.js b/scripts/recruit-crm-job-import/helper.js new file mode 100644 index 00000000..5cdf10ff --- /dev/null +++ b/scripts/recruit-crm-job-import/helper.js @@ -0,0 +1,142 @@ +/* + * Provide some commonly used functions for the RCRM import script. + */ +const config = require('./config') +const request = require('superagent') +const { getM2MToken } = require('../../src/common/helper') + +/** + * 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 a new job via taas api. + * + * @param {Object} data the job data + * @returns {Object} the result + */ +async function createJob (data) { + const token = await getM2MToken() + const { body: job } = await request.post(`${config.TAAS_API_URL}/jobs`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .send(data) + return job +} + +/** + * Find taas job by external id. + * + * @param {String} externalId the external id + * @returns {Object} the result + */ +async function getJobByExternalId (externalId) { + const token = await getM2MToken() + const { body: jobs } = await request.get(`${config.TAAS_API_URL}/jobs`) + .query({ externalId }) + .set('Authorization', `Bearer ${token}`) + if (!jobs.length) { + throw new Error(`externalId: ${externalId} job not found`) + } + return jobs[0] +} + +/** + * Update the status of a resource booking. + * + * @param {String} resourceBookingId the resource booking id + * @param {String} status the status for the resource booking + * @returns {Object} the result + */ +async function updateResourceBookingStatus (resourceBookingId, status) { + const token = await getM2MToken() + const { body: resourceBooking } = await request.patch(`${config.TAAS_API_URL}/resourceBookings/${resourceBookingId}`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .send({ status }) + return resourceBooking +} + +/** + * Find taas resource booking by job id and user id. + * + * @param {String} jobId the job id + * @param {String} userId the user id + * @returns {Object} the result + */ +async function getResourceBookingByJobIdAndUserId (jobId, userId) { + const token = await getM2MToken() + const { body: resourceBookings } = await request.get(`${config.TAAS_API_URL}/resourceBookings`) + .query({ jobId, userId }) + .set('Authorization', `Bearer ${token}`) + if (!resourceBookings.length) { + throw new Error(`jobId: ${jobId} userId: ${userId} resource booking not found`) + } + return resourceBookings[0] +} + +/** + * Create a new resource booking via taas api. + * + * @param {Object} data the resource booking data + * @returns {Object} the result + */ +async function createResourceBooking (data) { + const token = await getM2MToken() + const { body: resourceBooking } = await request.post(`${config.TAAS_API_URL}/resourceBookings`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .send(data) + return resourceBooking +} + +/** + * Find user via /v5/users by user handle. + * + * @param {String} handle the user handle + * @returns {Object} the result + */ +async function getUserByHandle (handle) { + const token = await getM2MToken() + const { body: users } = await request.get(`${config.TC_API}/users`) + .query({ handle }) + .set('Authorization', `Bearer ${token}`) + if (!users.length) { + throw new Error(`handle: ${handle} user not found`) + } + return users[0] +} + +/** + * Find project via /v5/projects by Direct project id. + * + * @param {Number} directProjectId the Direct project id + * @returns {Object} the result + */ +async function getProjectByDirectProjectId (directProjectId) { + const token = await getM2MToken() + const { body: projects } = await request.get(`${config.TC_API}/projects`) + .query({ directProjectId }) + .set('Authorization', `Bearer ${token}`) + if (!projects.length) { + throw new Error(`directProjectId: ${directProjectId} project not found`) + } + return projects[0] +} + +module.exports = { + sleep, + createJob, + getJobByExternalId, + updateResourceBookingStatus, + getResourceBookingByJobIdAndUserId, + createResourceBooking, + getUserByHandle, + getProjectByDirectProjectId +} diff --git a/scripts/recruit-crm-job-import/index.js b/scripts/recruit-crm-job-import/index.js new file mode 100644 index 00000000..e807e7f9 --- /dev/null +++ b/scripts/recruit-crm-job-import/index.js @@ -0,0 +1,177 @@ +/* + * Script to import Jobs data from Recruit CRM to Taas API. + */ + +const csv = require('csv-parser') +const fs = require('fs') +const Joi = require('joi') + .extend(require('@joi/date')) +const _ = require('lodash') +const dateFNS = require('date-fns') +const Report = require('./report') +const config = require('./config') +const helper = require('./helper') +const constants = require('./constants') +const logger = require('./logger') + +const jobSchema = Joi.object({ + directProjectId: Joi.number().integer().required(), + externalId: Joi.string().allow(''), + title: Joi.string().required(), + startDate: Joi.date().format('MM/DD/YYYY').required(), + endDate: Joi.date().format('MM/DD/YYYY').required(), + numPositions: Joi.number().integer().min(1), + userHandle: Joi.string(), + customerRate: Joi.number(), + memberRate: Joi.number(), + skills: Joi.array().default([]), + rateType: Joi.string().default('weekly') +}).unknown(true) + +/** + * Validate job data. + * + * @param {Object} job the job data + * @returns {Object} the validation result + */ +function validateJob (job) { + return jobSchema.validate(job) +} + +/** + * Load Recruit CRM jobs data from file. + * + * @param {String} pathname the pathname for the file + * @returns {Array} the result jobs data + */ +async function loadRcrmJobsFromFile (pathname) { + let lnum = 1 + const result = [] + return new Promise((resolve, reject) => { + fs.createReadStream(pathname) + .pipe(csv({ + mapHeaders: ({ header }) => constants.fieldNameMap[header] || header + })) + .on('data', (data) => { + result.push({ ...data, _lnum: lnum }) + lnum += 1 + }) + .on('error', err => reject(err)) + .on('end', () => resolve(result)) + }) +} + +/** + * Get pathname for a csv file from command line arguments. + * + * @returns {undefined} + */ +function getPathname () { + if (process.argv.length < 3) { + throw new Error('pathname for the csv file is required') + } + const pathname = process.argv[2] + if (!fs.existsSync(pathname)) { + throw new Error(`pathname: ${pathname} path not exist`) + } + if (!fs.lstatSync(pathname).isFile()) { + throw new Error(`pathname: ${pathname} path is not a regular file`) + } + return pathname +} + +/** + * Process single job data. The processing consists of: + * - Validate the data. + * - Skip processing if externalId is missing. + * - Create a job if it is not already exists. + * - Create a resource booking if it is not already exists. + * - Update the resourceBooking based on startDate and endDate. + * + * @param {Object} job the job data + * @param {Array} info contains processing details + * @returns {Object} + */ +async function processJob (job, info = []) { + // validate the data + const { value: data, error } = validateJob(job) + if (error) { + info.push(error.details[0].message) + return { status: constants.ProcessingStatus.Failed, info } + } + if (!data.externalId) { + info.push('externalId is missing') + return { status: constants.ProcessingStatus.Skipped, info } + } + data.projectId = (await helper.getProjectByDirectProjectId(data.directProjectId)).id + // create a job if it is not already exists + try { + const result = await helper.getJobByExternalId(data.externalId) + info.push(`id: ${result.id} externalId: ${data.externalId} job already exists`) + data.jobId = result.id + } catch (err) { + if (!(err.message && err.message.includes('job not found'))) { + throw err + } + const result = await helper.createJob(_.pick(data, ['projectId', 'externalId', 'title', 'numPositions', 'skills'])) + info.push(`id: ${result.id} job created`) + data.jobId = result.id + } + data.userId = (await helper.getUserByHandle(data.userHandle)).id + logger.debug(`userHandle: ${data.userHandle} userId: ${data.userId}`) + // create a resource booking if it is not already exists + try { + const result = await helper.getResourceBookingByJobIdAndUserId(data.jobId, data.userId) + info.push(`id: ${result.id} resource booking already exists`) + return { status: constants.ProcessingStatus.Successful, info } + } catch (err) { + if (!(err.message && err.message.includes('resource booking not found'))) { + throw err + } + const result = await helper.createResourceBooking(_.pick(data, ['projectId', 'jobId', 'userId', 'startDate', 'endDate', 'memberRate', 'customerRate', 'rateType'])) + info.push(`id: ${result.id} resource booking created`) + data.resourceBookingId = result.id + } + // update the resourceBooking based on startDate and endDate + const resourceBookingStatus = dateFNS.compareAsc(new Date(data.startDate), new Date(data.endDate)) === 1 ? 'closed' : 'assigned' + logger.debug(`resourceBookingId: ${data.resourceBookingId} status: ${resourceBookingStatus}`) + await helper.updateResourceBookingStatus(data.resourceBookingId, resourceBookingStatus) + info.push(`id: ${data.resourceBookingId} status: ${resourceBookingStatus} resource booking updated`) + return { status: constants.ProcessingStatus.Successful, info } +} + +/** + * The entry of the script. + * + * @returns {undefined} + */ +async function main () { + const pathname = getPathname() + const jobs = await loadRcrmJobsFromFile(pathname) + const report = new Report() + for (const job of jobs) { + logger.debug(`processing line #${job._lnum} - ${JSON.stringify(job)}`) + try { + const result = await processJob(job) + report.add({ lnum: job._lnum, ...result }) + } catch (err) { + if (err.response) { + report.add({ lnum: job._lnum, status: constants.ProcessingStatus.Failed, info: [err.response.error.toString().split('\n')[0]] }) + } else { + report.add({ lnum: job._lnum, status: constants.ProcessingStatus.Failed, info: [err.message] }) + } + } + report.print() + logger.debug(`processed line #${job._lnum}`) + await helper.sleep(config.SLEEP_TIME) + } + report.printSummary() +} + +main().then(() => { + logger.info('done!') + process.exit() +}).catch(err => { + logger.error(err.message) + process.exit(1) +}) diff --git a/scripts/recruit-crm-job-import/logger.js b/scripts/recruit-crm-job-import/logger.js new file mode 100644 index 00000000..ccb00102 --- /dev/null +++ b/scripts/recruit-crm-job-import/logger.js @@ -0,0 +1,10 @@ +/* + * Logger for the RCRM import script. + */ + +module.exports = { + info: (message) => console.log(`INFO: ${message}`), + debug: (message) => console.log(`DEBUG: ${message}`), + warn: (message) => console.log(`WARN: ${message}`), + error: (message) => console.log(`ERROR: ${message}`) +} diff --git a/scripts/recruit-crm-job-import/report.js b/scripts/recruit-crm-job-import/report.js new file mode 100644 index 00000000..574dd8f3 --- /dev/null +++ b/scripts/recruit-crm-job-import/report.js @@ -0,0 +1,49 @@ +/* + * The Report class. + */ + +const logger = require('./logger') +const constants = require('./constants') +const _ = require('lodash') + +class Report { + constructor () { + this.messages = [] + } + + // append a message to the report + add (message) { + this.messages.push(message) + } + + // print the last message to the console + print () { + const lastMessage = this.messages[this.messages.length - 1] + const output = `#${lastMessage.lnum} - ${lastMessage.info.join('; ')}` + if (lastMessage.status === constants.ProcessingStatus.Skipped) { + logger.warn(output) + } + if (lastMessage.status === constants.ProcessingStatus.Successful) { + logger.info(output) + } + if (lastMessage.status === constants.ProcessingStatus.Failed) { + logger.error(output) + } + } + + // print a summary to the console + printSummary () { + const groups = _.groupBy(this.messages, 'status') + const sucesss = groups[constants.ProcessingStatus.Successful] || [] + const failure = groups[constants.ProcessingStatus.Failed] || [] + const skips = groups[constants.ProcessingStatus.Skipped] || [] + logger.info('=== summary ===') + logger.info(`total: ${this.messages.length}`) + logger.info(`success: ${sucesss.length}`) + logger.info(`failure: ${failure.length}`) + logger.info(`skips: ${skips.length}`) + logger.info('=== summary ===') + } +} + +module.exports = Report diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 9f2a63cf..59c446d3 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -290,7 +290,7 @@ async function searchResourceBookings (currentUser, criteria, options = { return } } - _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId']), (value, key) => { + _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId']), (value, key) => { esQuery.body.query.bool.must.push({ term: { [key]: { @@ -328,7 +328,7 @@ async function searchResourceBookings (currentUser, criteria, options = { return const filter = { [Op.and]: [{ deletedAt: null }] } - _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType']), (value, key) => { + _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId']), (value, key) => { filter[Op.and].push({ [key]: value }) }) if (criteria.projectIds) { @@ -363,6 +363,8 @@ searchResourceBookings.schema = Joi.object().keys({ startDate: Joi.date(), endDate: Joi.date(), rateType: Joi.rateType(), + jobId: Joi.string().uuid(), + userId: Joi.string().uuid(), projectId: Joi.number().integer(), projectIds: Joi.alternatives( Joi.string(),