diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 39fabfb8..c780910f 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "3a5bab78-49d6-4dca-9aea-e8688564ac98", + "_postman_id": "6f274c86-24a5-412e-95e6-fafa34e2a936", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -8182,10 +8182,35 @@ "value": "id,workPeriods.id,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.payments.id,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.payments.id,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.payments", + "disabled": true } ] } @@ -8351,10 +8376,35 @@ "value": "id,status,workPeriods.id,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods.id,workPeriods.payments.id,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods.id,workPeriods.payments.id,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,status,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods.payments", + "disabled": true } ] } @@ -8525,10 +8575,35 @@ "value": "id,rateType,workPeriods.id,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods.id,workPeriods.payments.id,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods.id,workPeriods.payments.id,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,rateType,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods.payments", + "disabled": true } ] } @@ -8699,10 +8774,35 @@ "value": "id,startDate,workPeriods.id,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,startDate,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods.payments", + "disabled": true } ] } @@ -8873,10 +8973,35 @@ "value": "id,endDate,workPeriods.id,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,endDate,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods.payments", + "disabled": true } ] } @@ -9047,10 +9172,35 @@ "value": "id,customerRate,workPeriods.id,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,customerRate,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods.payments", + "disabled": true } ] } @@ -9221,10 +9371,35 @@ "value": "id,memberRate,workPeriods.id,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods.id,workPeriods.payments.id,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,memberRate,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods.payments", + "disabled": true } ] } @@ -9388,10 +9563,35 @@ "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.startDate,workPeriods.payments", + "disabled": true } ] } @@ -9555,10 +9755,35 @@ "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.startDate,workPeriods.daysWorked,workPeriods.payments", + "disabled": true } ] } @@ -9722,10 +9947,35 @@ "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.startDate,workPeriods.daysPaid,workPeriods.payments", + "disabled": true } ] } @@ -9889,10 +10139,35 @@ "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.startDate,workPeriods.paymentTotal,workPeriods.payments", + "disabled": true } ] } @@ -10056,10 +10331,35 @@ "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,workPeriods.userHandle", "disabled": true }, + { + "key": "workPeriods.payments.status", + "value": "completed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,workPeriods.payments.status", + "disabled": true + }, + { + "key": "workPeriods.payments.days", + "value": "3", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,workPeriods.payments.days", + "disabled": true + }, { "key": "fields", "value": "id,workPeriods", "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.startDate,workPeriods.paymentStatus,workPeriods.payments", + "disabled": true } ] } @@ -11092,7 +11392,7 @@ "pm.test('Status code is 403', function () {\r", " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You don't have access to view memberRate and paymentTotal\")\r", + " pm.expect(response.message).to.eq(\"You don't have access to view memberRate, paymentTotal and payments\")\r", "});" ], "type": "text/javascript" @@ -11137,7 +11437,7 @@ "pm.test('Status code is 403', function () {\r", " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You don't have access to view memberRate and paymentTotal\")\r", + " pm.expect(response.message).to.eq(\"You don't have access to view memberRate, paymentTotal and payments\")\r", "});" ], "type": "text/javascript" @@ -11967,7 +12267,7 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"Can not filter or sort by some field which is not included in fields\")\r", + " pm.expect(response.message).to.eq(\"Can not filter or sort by ResourceBooking field which is not included in fields\")\r", "});" ], "type": "text/javascript" @@ -12015,7 +12315,7 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"Can not filter or sort by some field which is not included in fields\")\r", + " pm.expect(response.message).to.eq(\"Can not filter or sort by ResourceBooking field which is not included in fields\")\r", "});" ], "type": "text/javascript" @@ -12155,7 +12455,7 @@ "pm.test('Status code is 403', function () {\r", " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You don't have access to view memberRate and paymentTotal\")\r", + " pm.expect(response.message).to.eq(\"You don't have access to view memberRate, paymentTotal and payments\")\r", "});" ], "type": "text/javascript" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a8abc4e4..443b9819 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1400,7 +1400,7 @@ paths: parameters: - in: query name: fields - description: the field names to be returned from both ResourceBooking and WorkPeriod + description: the field names to be returned from all ResourceBooking, WorkPeriod and WorkPeriodPayment required: false schema: type: string @@ -1537,6 +1537,21 @@ paths: schema: type: string description: The user handle. + - in: query + name: workPeriods.payments.status + required: false + schema: + type: string + description: The status of the payment + - in: query + name: workPeriods.payments.days + required: false + schema: + type: integer + minimum: 1 + maximum: 5 + example: 3 + description: The workdays to pay responses: "200": @@ -1627,7 +1642,7 @@ paths: type: boolean - in: query name: fields - description: the field names to be returned from both ResourceBooking and WorkPeriod + description: the field names to be returned from all ResourceBooking, WorkPeriod and WorkPeriodPayment required: false schema: type: string diff --git a/src/models/ResourceBooking.js b/src/models/ResourceBooking.js index 54a95b89..580e6e96 100644 --- a/src/models/ResourceBooking.js +++ b/src/models/ResourceBooking.js @@ -42,13 +42,32 @@ module.exports = (sequelize) => { }] // Select WorkPeriod fields if (!options.allWorkPeriods) { - criteria.include[0].attributes = _.map(options.fieldsWP, f => _.split(f, '.')[1]) + if (options.fieldsWP && options.fieldsWP.length > 0) { + criteria.include[0].attributes = _.map(options.fieldsWP, f => _.split(f, '.')[1]) + } else { + // we should include at least one workPeriod field + // if fields criteria has no workPeriod field but have workPeriodPayment field + criteria.include[0].attributes = ['id'] + } } else if (options.excludeWP && options.excludeWP.length > 0) { criteria.include[0].attributes = { exclude: _.map(options.excludeWP, f => _.split(f, '.')[1]) } } + // Include WorkPeriodPayment Model + if (options.withWorkPeriodPayments) { + criteria.include[0].include = [{ + model: ResourceBooking._models.WorkPeriodPayment, + as: 'payments', + required: false + }] + // Select WorkPeriodPayment fields + if (!options.allWorkPeriodPayments) { + criteria.include[0].include[0].attributes = _.map(options.fieldsWPP, f => _.split(f, '.')[2]) + } else if (options.excludeWPP && options.excludeWPP.length > 0) { + criteria.include[0].include[0].attributes = { exclude: _.map(options.excludeWPP, f => _.split(f, '.')[2]) } + } + } } } - const resourceBooking = await ResourceBooking.findOne(criteria) if (!resourceBooking) { throw new errors.NotFoundError(`id: ${id} "ResourceBooking" doesn't exists.`) diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index e226f410..cabadca0 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -29,7 +29,8 @@ const cachedModelFields = _cacheModelFields() function _cacheModelFields () { const resourceBookingFields = _.keys(ResourceBooking.rawAttributes) const workPeriodFields = _.map(_.keys(WorkPeriod.rawAttributes), key => `workPeriods.${key}`) - return [...resourceBookingFields, 'workPeriods', ...workPeriodFields] + const workPeriodPaymentFields = _.map(_.keys(WorkPeriodPayment.rawAttributes), key => `workPeriods.payments.${key}`) + return [...resourceBookingFields, 'workPeriods', ...workPeriodFields, 'workPeriods.payments', ...workPeriodPaymentFields] } /** @@ -42,6 +43,16 @@ function _checkUserScopesForGetWorkPeriods (currentUser) { return currentUser.isMachine && helper.checkIfExists(getWorkPeriodsScopes, currentUser.scopes) } +/** + * Check user scopes for getting workPeriodPayments + * @param {Object} currentUser the user who perform this operation. + * @returns {Boolean} true if user is machine and has read/all workPeriodPayment scopes + */ +function _checkUserScopesForGetWorkPeriodPayments (currentUser) { + const getWorkPeriodPaymentsScopes = [constants.Scopes.READ_WORK_PERIOD_PAYMENT, constants.Scopes.ALL_WORK_PERIOD_PAYMENT] + return currentUser.isMachine && helper.checkIfExists(getWorkPeriodPaymentsScopes, currentUser.scopes) +} + /** * Evaluates the criterias and returns the fields * to be returned as a result of GET endpoints @@ -51,25 +62,32 @@ function _checkUserScopesForGetWorkPeriods (currentUser) { * @returns {Array} result.include field names to include * @returns {Array} result.fieldsRB ResourceBooking field names to include * @returns {Array} result.fieldsWP WorkPeriod field names to include + * @returns {Array} result.fieldsWPP WorkPeriodPayment field names to include * @returns {Array} result.excludeRB ResourceBooking field names to exclude * @returns {Array} result.excludeWP WorkPeriod field names to exclude * @returns {Boolean} result.regularUser is current user a regular user? * @returns {Boolean} result.allWorkPeriods will all WorkPeriod fields be returned? * @returns {Boolean} result.withWorkPeriods does fields include any WorkPeriod field? + * @returns {Boolean} result.allWorkPeriodPayments will all WorkPeriodPayment fields be returned? + * @returns {Boolean} result.withWorkPeriodPayments does fields include any WorkPeriodPayment field? * @returns {Boolean} result.sortByWP will the sorting be done by WorkPeriod field? + * @throws {BadRequestError} + * @throws {ForbiddenError} */ function _checkCriteriaAndGetFields (currentUser, criteria) { const result = { include: [], fieldsRB: [], fieldsWP: [], + fieldsWPP: [], excludeRB: [], - excludeWP: [] + excludeWP: [], + excludeWPP: [] } const fields = criteria.fields const sort = criteria.sortBy const onlyResourceBooking = _.isUndefined(fields) - const query = onlyResourceBooking ? [] : _.split(fields, ',') + const query = onlyResourceBooking ? [] : _.uniq(_.filter(_.map(_.split(fields, ','), _.trim), field => !_.isEmpty(field))) const notAllowedFields = _.difference(query, cachedModelFields) // Check if fields criteria has a field name that RB or WP models don't have if (notAllowedFields.length > 0) { @@ -79,12 +97,17 @@ function _checkCriteriaAndGetFields (currentUser, criteria) { result.regularUser = !currentUser.hasManagePermission && !currentUser.isMachine && !currentUser.isConnectManager // Check if all WorkPeriod fields will be returned result.allWorkPeriods = _.some(query, q => q === 'workPeriods') + // Check if all WorkPeriodPayment fields will be returned + result.allWorkPeriodPayments = result.allWorkPeriods || _.some(query, q => q === 'workPeriods.payments') // Split the fields criteria into ResourceBooking and WorkPeriod fields _.forEach(query, q => { - if (_.includes(q, '.')) { result.fieldsWP.push(q) } else if (q !== 'workPeriods') { result.fieldsRB.push(q) } + if (_.includes(q, 'payments.')) { result.fieldsWPP.push(q) } else if (q !== 'workPeriods.payments' && _.includes(q, '.')) { result.fieldsWP.push(q) } else if (q !== 'workPeriods' && q !== 'workPeriods.payments') { result.fieldsRB.push(q) } }) // Check if any WorkPeriod field will be returned - result.withWorkPeriods = result.allWorkPeriods || result.fieldsWP.length > 0 + result.withWorkPeriods = result.allWorkPeriods || result.fieldsWP.length > 0 || + result.allWorkPeriodPayments || result.fieldsWPP.length > 0 + // Check if any WorkPeriodPayment field will be returned + result.withWorkPeriodPayments = result.allWorkPeriodPayments || result.fieldsWPP.length > 0 // Extract the filters from criteria parameter let filters = _.filter(Object.keys(criteria), key => _.indexOf(['fromDb', 'fields', 'page', 'perPage', 'sortBy', 'sortOrder'], key) === -1) filters = _.map(filters, f => { @@ -94,20 +117,22 @@ function _checkCriteriaAndGetFields (currentUser, criteria) { }) const filterRB = [] const filterWP = [] - // Split the filters criteria into ResourceBooking and WorkPeriod filters - _.forEach(filters, q => { if (_.includes(q, '.')) { filterWP.push(q) } else { filterRB.push(q) } }) - // Check if filter criteria has any WorkPeriod filter - const filterHasWorkPeriods = filterWP.length > 0 + const filterWPP = [] + // Split the filters criteria into ResourceBooking, WorkPeriod and WorkPeriodPayment filters + _.forEach(filters, q => { if (_.includes(q, 'payments.')) { filterWPP.push(q) } else if (_.includes(q, '.')) { filterWP.push(q) } else { filterRB.push(q) } }) + // Check if filter criteria has any WorkPeriod or payments filter + const filterHasWorkPeriods = filterWP.length > 0 || filterWPP.length > 0 // Check if sorting will be done by WorkPeriod field result.sortByWP = _.split(sort, '.')[0] === 'workPeriods' // Check if the current user has the right to see the memberRate const canSeeMemberRate = currentUser.hasManagePermission || currentUser.isMachine - // If current user has no right to see the memberRate then it's excluded + // If current user has no right to see the memberRate then it's excluded. // "currentUser.isMachine" to be true is not enough to return "workPeriods.memberRate" // but returning "workPeriod" will be evaluated later if (!canSeeMemberRate) { result.excludeRB.push('paymentTotal') result.excludeWP.push('workPeriods.paymentTotal') + result.excludeWPP.push('workPeriods.payments') } // if "fields" is not included in cretia, then only ResourceBooking model will be returned // No further evaluation is required as long as the criteria does not include a WorkPeriod filter or a WorkPeriod sorting condition @@ -130,20 +155,28 @@ function _checkCriteriaAndGetFields (currentUser, criteria) { } // Check If it's tried to filter or sort by some field which should not be included as per rules of fields param if (_.difference(filterRB, result.fieldsRB).length > 0) { - throw new errors.BadRequestError('Can not filter or sort by some field which is not included in fields') + throw new errors.BadRequestError('Can not filter or sort by ResourceBooking field which is not included in fields') } // Check If it's tried to filter or sort by some field which should not be included as per rules of fields param if (!result.allWorkPeriods && _.difference(filterWP, result.fieldsWP).length > 0) { - throw new errors.BadRequestError('Can not filter or sort by some field which is not included in fields') + throw new errors.BadRequestError('Can not filter or sort by WorkPeriod field which is not included in fields') + } + // Check If it's tried to filter or sort by some field which should not be included as per rules of fields param + if (!result.allWorkPeriodPayments && _.difference(filterWPP, result.fieldsWPP).length > 0) { + throw new errors.BadRequestError('Can not filter by WorkPeriodPayment field which is not included in fields') } // Check if the current user has no right to see the memberRate and memberRate is included in fields parameter - if (!canSeeMemberRate && _.some(query, q => _.includes(['memberRate', 'workPeriods.paymentTotal'], q))) { - throw new errors.ForbiddenError('You don\'t have access to view memberRate and paymentTotal') + if (!canSeeMemberRate && _.some(query, q => _.includes(['memberRate', 'workPeriods.paymentTotal', 'workPeriods.payments'], q))) { + throw new errors.ForbiddenError('You don\'t have access to view memberRate, paymentTotal and payments') } // Check if the current user has no right to see the workPeriods and workPeriods is included in fields parameter if (currentUser.isMachine && result.withWorkPeriods && !_checkUserScopesForGetWorkPeriods(currentUser)) { throw new errors.ForbiddenError('You don\'t have access to view workPeriods') } + // Check if the current user has no right to see the workPeriodPayments and workPeriodPayments is included in fields parameter + if (currentUser.isMachine && result.withWorkPeriodPayments && !_checkUserScopesForGetWorkPeriodPayments(currentUser)) { + throw new errors.ForbiddenError('You don\'t have access to view workPeriodPayments') + } result.include.push(...query) return result } @@ -249,7 +282,7 @@ async function getResourceBooking (currentUser, id, criteria) { index: config.esConfig.ES_INDEX_RESOURCE_BOOKING, id, _source_includes: [...queryOpt.include], - _source_excludes: ['workPeriods.payments', ...queryOpt.excludeRB, ...queryOpt.excludeWP] + _source_excludes: [...queryOpt.excludeRB, ...queryOpt.excludeWP, ...queryOpt.excludeWPP] }) if (queryOpt.regularUser) { await _checkUserPermissionForGetResourceBooking(currentUser, resourceBooking.body._source.projectId) // check user permission @@ -266,11 +299,18 @@ async function getResourceBooking (currentUser, id, criteria) { } } logger.info({ component: 'ResourceBookingService', context: 'getResourceBooking', message: 'try to query db for data' }) - const resourceBooking = await ResourceBooking.findById(id, queryOpt) + let resourceBooking = await ResourceBooking.findById(id, queryOpt) + resourceBooking = resourceBooking.toJSON() + // omit workPeriod.id if fields criteria has no workPeriod field but have workPeriodPayment field + if (queryOpt.withWorkPeriods && !queryOpt.allWorkPeriods && (!queryOpt.fieldsWP || queryOpt.fieldsWP.length === 0)) { + if (_.isArray(resourceBooking.workPeriods)) { + resourceBooking.workPeriods = _.map(resourceBooking.workPeriods, wp => _.omit(wp, 'id')) + } + } if (queryOpt.regularUser) { await _checkUserPermissionForGetResourceBooking(currentUser, resourceBooking.projectId) // check user permission } - return resourceBooking.dataValues + return resourceBooking } getResourceBooking.schema = Joi.object().keys({ @@ -512,7 +552,7 @@ async function searchResourceBookings (currentUser, criteria, options) { const esQuery = { index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), _source_includes: queryOpt.include, - _source_excludes: ['workPeriods.payments', ...queryOpt.excludeRB, ...queryOpt.excludeWP], + _source_excludes: [...queryOpt.excludeRB, ...queryOpt.excludeWP, ...queryOpt.excludeWPP], body: { query: { bool: { @@ -566,9 +606,10 @@ async function searchResourceBookings (currentUser, criteria, options) { } }] } - // Apply WorkPeriod filters + // Apply WorkPeriod and WorkPeriodPayment filters const workPeriodFilters = _.pick(criteria, ['workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle']) - if (!_.isEmpty(workPeriodFilters)) { + const workPeriodPaymentFilters = _.pick(criteria, ['workPeriods.payments.status', 'workPeriods.payments.days']) + if (!_.isEmpty(workPeriodFilters) || !_.isEmpty(workPeriodPaymentFilters)) { const workPeriodsMust = [] _.each(workPeriodFilters, (value, key) => { if (key === 'workPeriods.paymentStatus') { @@ -587,11 +628,33 @@ async function searchResourceBookings (currentUser, criteria, options) { }) } }) - + const workPeriodPaymentPath = [] + if (!_.isEmpty(workPeriodPaymentFilters)) { + const workPeriodPaymentsMust = [] + _.each(workPeriodPaymentFilters, (value, key) => { + workPeriodPaymentsMust.push({ + term: { + [key]: { + value + } + } + }) + }) + workPeriodPaymentPath.push({ + nested: { + path: 'workPeriods.payments', + query: { bool: { must: workPeriodPaymentsMust } } + } + }) + } esQuery.body.query.bool.must.push({ nested: { path: 'workPeriods', - query: { bool: { must: workPeriodsMust } } + query: { + bool: { + must: [...workPeriodsMust, ...workPeriodPaymentPath] + } + } } }) } @@ -601,18 +664,24 @@ async function searchResourceBookings (currentUser, criteria, options) { const resourceBookings = _.map(body.hits.hits, '_source') // ESClient will return ResourceBookings with it's all nested WorkPeriods // We re-apply WorkPeriod filters except userHandle because all WPs share same userHandle - _.each(_.omit(workPeriodFilters, 'workPeriods.userHandle'), (value, key) => { - key = key.split('.')[1] + if (!_.isEmpty(workPeriodFilters) || !_.isEmpty(workPeriodPaymentFilters)) { _.each(resourceBookings, r => { r.workPeriods = _.filter(r.workPeriods, wp => { - if (key === 'paymentStatus') { - return _.includes(value, wp[key]) - } else { - return wp[key] === value - } + return _.every(_.omit(workPeriodFilters, 'workPeriods.userHandle'), (value, key) => { + key = key.split('.')[1] + if (key === 'paymentStatus') { + return _.includes(value, wp[key]) + } else { + return wp[key] === value + } + }) && _.every(workPeriodPaymentFilters, (value, key) => { + key = key.split('.')[2] + wp.payments = _.filter(wp.payments, payment => payment[key] === value) + return wp.payments.length > 0 + }) }) }) - }) + } // sort Work Periods inside Resource Bookings by startDate just for comfort output _.each(resourceBookings, r => { @@ -662,7 +731,13 @@ async function searchResourceBookings (currentUser, criteria, options) { }] // Select WorkPeriod fields if (!queryOpt.allWorkPeriods) { - queryCriteria.include[0].attributes = _.map(queryOpt.fieldsWP, f => _.split(f, '.')[1]) + if (queryOpt.fieldsWP && queryOpt.fieldsWP.length > 0) { + queryCriteria.include[0].attributes = _.map(queryOpt.fieldsWP, f => _.split(f, '.')[1]) + } else { + // we should include at least one workPeriod field + // if fields criteria has no workPeriod field but have workPeriodPayment field + queryCriteria.include[0].attributes = ['id'] + } } else if (queryOpt.excludeWP && queryOpt.excludeWP.length > 0) { queryCriteria.include[0].attributes = { exclude: _.map(queryOpt.excludeWP, f => _.split(f, '.')[1]) } } @@ -677,6 +752,30 @@ async function searchResourceBookings (currentUser, criteria, options) { if (queryCriteria.include[0].where[Op.and].length > 0) { queryCriteria.include[0].required = true } + // Include WorkPeriodPayment Model + if (queryOpt.withWorkPeriodPayments) { + queryCriteria.include[0].include = [{ + model: WorkPeriodPayment, + as: 'payments', + required: false, + where: { [Op.and]: [] } + }] + // Select WorkPeriodPayment fields + if (!queryOpt.allWorkPeriodPayments) { + queryCriteria.include[0].include[0].attributes = _.map(queryOpt.fieldsWPP, f => _.split(f, '.')[2]) + } else if (queryOpt.excludeWPP && queryOpt.excludeWPP.length > 0) { + queryCriteria.include[0].include[0].attributes = { exclude: _.map(queryOpt.excludeWPP, f => _.split(f, '.')[2]) } + } + // Apply WorkPeriodPayment filters + _.each(_.pick(criteria, ['workPeriods.payments.status', 'workPeriods.payments.days']), (value, key) => { + key = key.split('.')[2] + queryCriteria.include[0].include[0].where[Op.and].push({ [key]: value }) + }) + if (queryCriteria.include[0].include[0].where[Op.and].length > 0) { + queryCriteria.include[0].required = true + queryCriteria.include[0].include[0].required = true + } + } } // Apply sorting criteria if (!queryOpt.sortByWP) { @@ -685,7 +784,16 @@ async function searchResourceBookings (currentUser, criteria, options) { queryCriteria.subQuery = false queryCriteria.order = [[{ model: WorkPeriod, as: 'workPeriods' }, _.split(criteria.sortBy, '.')[1], `${criteria.sortOrder} NULLS LAST`]] } - const result = await ResourceBooking.findAll(queryCriteria) + const resultModel = await ResourceBooking.findAll(queryCriteria) + const result = _.map(resultModel, r => r.toJSON()) + // omit workPeriod.id if fields criteria has no workPeriod field but have workPeriodPayment field + if (queryOpt.withWorkPeriods && !queryOpt.allWorkPeriods && (!queryOpt.fieldsWP || queryOpt.fieldsWP.length === 0)) { + _.each(result, r => { + if (_.isArray(r.workPeriods)) { + r.workPeriods = _.map(r.workPeriods, wp => _.omit(wp, 'id')) + } + }) + } // sort Work Periods inside Resource Bookings by startDate just for comfort output _.each(result, r => { if (_.isArray(r.workPeriods)) { @@ -735,11 +843,13 @@ searchResourceBookings.schema = Joi.object().keys({ ), 'workPeriods.startDate': Joi.date().format('YYYY-MM-DD'), 'workPeriods.endDate': Joi.date().format('YYYY-MM-DD'), - 'workPeriods.userHandle': Joi.string() + 'workPeriods.userHandle': Joi.string(), + 'workPeriods.payments.status': Joi.workPeriodPaymentStatus(), + 'workPeriods.payments.days': Joi.number().integer().min(0).max(5) }).required(), options: Joi.object().keys({ returnAll: Joi.boolean().default(false), - fromDb: Joi.boolean().default(false) + returnFromDB: Joi.boolean().default(false) }).default({ returnAll: false, returnFromDB: false diff --git a/test/unit/common/ResourceBookingData.js b/test/unit/common/ResourceBookingData.js index 0970c73e..1f9535a1 100644 --- a/test/unit/common/ResourceBookingData.js +++ b/test/unit/common/ResourceBookingData.js @@ -1206,6 +1206,7 @@ const T18 = { }, criteria: { fromDb: true } } +T18.resourceBooking.value.toJSON = () => T18.resourceBooking.value.dataValues const T19 = { id: '520bb632-a02a-415e-9857-93b2ecbf7d60', criteria: { @@ -1223,7 +1224,7 @@ const T20 = { }, error: { httpStatus: 403, - message: 'You don\'t have access to view memberRate and paymentTotal' + message: 'You don\'t have access to view memberRate, paymentTotal and payments' } } const T21 = { @@ -1371,38 +1372,44 @@ const T24 = { const T25 = { resourceBookingFindAll: [ { - updatedBy: null, - endDate: '2020-10-27', - billingAccountId: 80000071, - userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', - jobId: '05232809-3693-44c1-a0cc-9a79f2672385', - rateType: 'hourly', - createdAt: '2021-05-08T18:35:16.368Z', - memberRate: 13.23, - createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', - customerRate: 13, - id: 'fbe133dd-0e36-4d0c-8197-49307b13ce75', - projectId: 17234, - startDate: '2020-09-27', - status: 'placed', - updatedAt: '2021-05-08T18:35:16.368Z' + dataValues: { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '05232809-3693-44c1-a0cc-9a79f2672385', + rateType: 'hourly', + createdAt: '2021-05-08T18:35:16.368Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: 'fbe133dd-0e36-4d0c-8197-49307b13ce75', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-05-08T18:35:16.368Z' + }, + toJSON: () => T25.resourceBookingFindAll[0].dataValues }, { - updatedBy: null, - endDate: '2020-10-27', - billingAccountId: 80000071, - userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', - jobId: '05232809-3693-44c1-a0cc-9a79f2672385', - rateType: 'hourly', - createdAt: '2021-05-08T18:47:37.268Z', - memberRate: 13.23, - createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', - customerRate: 13, - id: '60e99790-8da0-4596-badc-29a06feb78a0', - projectId: 17234, - startDate: '2020-09-27', - status: 'placed', - updatedAt: '2021-05-08T18:47:37.268Z' + dataValues: { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '05232809-3693-44c1-a0cc-9a79f2672385', + rateType: 'hourly', + createdAt: '2021-05-08T18:47:37.268Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: '60e99790-8da0-4596-badc-29a06feb78a0', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-05-08T18:47:37.268Z' + }, + toJSON: () => T25.resourceBookingFindAll[1].dataValues } ], resourceBookingCount: [{ id: 'fbe133dd-0e36-4d0c-8197-49307b13ce75', count: 1 }, @@ -1466,7 +1473,7 @@ const T27 = { }, error: { httpStatus: 403, - message: 'You don\'t have access to view memberRate and paymentTotal' + message: 'You don\'t have access to view memberRate, paymentTotal and payments' } } const T28 = { @@ -1517,14 +1524,14 @@ const T34 = { criteria: { fields: 'id,startDate,endDate,workPeriods', status: 'closed' }, error: { httpStatus: 400, - message: 'Can not filter or sort by some field which is not included in fields' + message: 'Can not filter or sort by ResourceBooking field which is not included in fields' } } const T35 = { criteria: { fields: 'id,startDate,endDate', 'workPeriods.paymentStatus': 'completed' }, error: { httpStatus: 400, - message: 'Can not filter or sort by some field which is not included in fields' + message: 'Can not filter or sort by WorkPeriod field which is not included in fields' } } const T36 = {