diff --git a/app-constants.js b/app-constants.js index 9b577728..3265d4c4 100644 --- a/app-constants.js +++ b/app-constants.js @@ -82,9 +82,10 @@ const ChallengeStatus = { COMPLETED: 'Completed' } -const PaymentProcessingSwitch = { - ON: 'ON', - OFF: 'OFF' +const WorkPeriodPaymentStatus = { + COMPLETED: 'completed', + CANCELLED: 'cancelled', + SCHEDULED: 'scheduled' } module.exports = { @@ -93,5 +94,5 @@ module.exports = { Scopes, Interviews, ChallengeStatus, - PaymentProcessingSwitch + WorkPeriodPaymentStatus } diff --git a/config/default.js b/config/default.js index 93f42e20..0af05190 100644 --- a/config/default.js +++ b/config/default.js @@ -169,7 +169,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/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index c1233474..4b0c588e 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -16907,6 +16907,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": [ @@ -17007,6 +17056,97 @@ }, "response": [] }, + { + "name": "create multiple work period payments with boooking 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": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "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" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments" + ] + } + }, + "response": [] + }, + { + "name": "create query work period payments with boooking 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": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "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": "create work period payment with m2m create", "event": [ @@ -27377,6 +27517,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": [ @@ -29829,6 +30015,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": [ @@ -32343,6 +32577,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": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1d036ce6..34245fb8 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2293,14 +2293,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: @@ -2393,7 +2403,7 @@ paths: required: false schema: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "cancelled"] description: The payment status. responses: "200": @@ -2457,6 +2467,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: @@ -4672,7 +4735,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 @@ -4694,6 +4757,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 @@ -4708,8 +4793,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: @@ -4722,7 +4884,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 cd69b900..259c4a2c 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) @@ -21,7 +19,7 @@ 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. @@ -46,14 +44,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/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',