diff --git a/app-constants.js b/app-constants.js index e9747f85..106e77da 100644 --- a/app-constants.js +++ b/app-constants.js @@ -36,8 +36,15 @@ const Scopes = { READ_TAAS_TEAM: 'read:taas-teams' } +const ChallengeStatus = { + DRAFT: 'Draft', + ACTIVE: 'Active', + COMPLETED: 'Completed' +} + module.exports = { UserRoles, FullManagePermissionRoles, - Scopes + Scopes, + ChallengeStatus } diff --git a/config/default.js b/config/default.js index e475a677..14599bef 100644 --- a/config/default.js +++ b/config/default.js @@ -124,5 +124,11 @@ module.exports = { // SendGrid email template ID for requesting extension REQUEST_EXTENSION_SENDGRID_TEMPLATE_ID: process.env.REQUEST_EXTENSION_SENDGRID_TEMPLATE_ID, // the URL where TaaS App is hosted - TAAS_APP_URL: process.env.TAAS_APP_URL || 'https://platform.topcoder-dev.com/taas/myteams' + TAAS_APP_URL: process.env.TAAS_APP_URL || 'https://platform.topcoder-dev.com/taas/myteams', + // environment variables for Payment Service + 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' + } diff --git a/package.json b/package.json index 23621ca5..2ed4ee77 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "services:logs": "docker-compose -f ./local/docker-compose.yml logs", "local:init": "npm run local:reset && npm run data:import -- --force", "local:reset": "npm run delete-index -- --force || true && npm run create-index -- --force && npm run init-db force", - "cov": "nyc --reporter=html --reporter=text mocha test/unit/*.test.js --timeout 30000 --exit" + "cov": "nyc --reporter=html --reporter=text mocha test/unit/*.test.js --timeout 30000 --exit", + "demo-payment": "node scripts/demo-payment" }, "keywords": [], "author": "", @@ -84,4 +85,4 @@ "test/unit/**" ] } -} +} \ No newline at end of file diff --git a/scripts/demo-payment/README.md b/scripts/demo-payment/README.md new file mode 100644 index 00000000..d39e432b --- /dev/null +++ b/scripts/demo-payment/README.md @@ -0,0 +1,22 @@ +### DEMO PAYMENT SCRIPT + +This demo script tests the functionality of PaymentService. + +Parameters for creating payments are hardcoded in the script. There are severel groups of parameters, each of them tests a certain functionality of the demo service. You can always insert new group of parameters to run in the script. + +Before start set the following environment variables: +AUTH0_URL= +AUTH0_AUDIENCE= +AUTH0_AUDIENCE_UBAHN= +AUTH0_CLIENT_ID= +AUTH0_CLIENT_SECRET= + +To run the script use the following commands: + +``` +npm install +npm run lint +npm run demo-payment +``` + +Read the logger to see results. \ No newline at end of file diff --git a/scripts/demo-payment/index.js b/scripts/demo-payment/index.js new file mode 100644 index 00000000..f84880b9 --- /dev/null +++ b/scripts/demo-payment/index.js @@ -0,0 +1,114 @@ +require('../../src/bootstrap') +const logger = require('../../src/common/logger') +const paymentService = require('../../src/services/PaymentService') + +const options = [ + { + name: 'Test joi validation for projectId-1', + content: { + userHandle: 'pshah_manager', + amount: 3, + billingAccountId: 80000069, + name: 'test payment for pshah_manager', + description: '## test payment' + } + }, + { + name: 'Test joi validation for projectId-2', + content: { + projectId: 'project', + userHandle: 'pshah_manager', + amount: 3, + billingAccountId: 80000069, + name: 'test payment for pshah_manager', + description: '## test payment' + } + }, + { + name: 'Test joi validation for userHandle', + content: { + projectId: 17234, + amount: 3, + billingAccountId: 80000069, + name: 'test payment for pshah_manager', + description: '## test payment' + } + }, + { + name: 'Test joi validation for amount-1', + content: { + projectId: 17234, + userHandle: 'pshah_manager', + billingAccountId: 80000069, + name: 'test payment for pshah_manager', + description: '## test payment' + } + }, + { + name: 'Test joi validation for amount-2', + content: { + projectId: 17234, + userHandle: 'pshah_manager', + amount: -10, + billingAccountId: 80000069, + name: 'test payment for pshah_manager', + description: '## test payment' + } + }, + { + name: 'Successful payment creation', + content: { + projectId: 17234, + userHandle: 'pshah_manager', + amount: 3, + billingAccountId: 80000069, + name: 'test payment for pshah_manager', + description: '## test payment' + } + }, + { + name: 'Successful payment creation without name and description', + content: { + projectId: 17234, + userHandle: 'pshah_customer', + amount: 2, + billingAccountId: 80000069 + } + }, + { + name: 'Failing payment creation with no active billing account', + content: { + projectId: 16839, + userHandle: 'pshah_customer', + amount: 2, + billingAccountId: 80000069, + name: 'test payment for pshah_customer', + description: '## test payment' + } + }, + { + name: 'Failing payment creation with non existing user', + content: { + projectId: 17234, + userHandle: 'eisbilir', + amount: 2, + billingAccountId: 80000069 + } + } +] + +const test = async () => { + for (const option of options) { + logger.info({ component: 'demo-payment', context: 'test', message: `Starting to create payment for: ${option.name}` }) + await paymentService.createPayment(option.content) + .then(data => { + logger.info({ component: 'demo-payment', context: 'test', message: `Payment successfuly created for: ${option.name}` }) + }) + // eslint-disable-next-line handle-callback-err + .catch(err => { + logger.error({ component: 'demo-payment', context: 'test', message: `Payment can't be created for: ${option.name}` }) + }) + } +} +// wait for bootstrap to complete it's job. +setTimeout(test, 2000) diff --git a/src/common/helper.js b/src/common/helper.js index 0f456215..954cd4ba 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1118,6 +1118,82 @@ async function deleteProjectMember (currentUser, projectId, projectMemberId) { } } +/** + * Create a new challenge + * + * @param {Object} data challenge data + * @param {String} token m2m token + * @returns {Object} the challenge created + */ +async function createChallenge (data, token) { + if (!token) { + token = await getM2MToken() + } + const url = `${config.TC_API}/challenges` + localLogger.debug({ context: 'createChallenge', message: `EndPoint: POST ${url}` }) + localLogger.debug({ context: 'createChallenge', message: `Request Body: ${JSON.stringify(data)}` }) + const { body: challenge, status: httpStatus } = await request + .post(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .send(data) + localLogger.debug({ context: 'createChallenge', message: `Status Code: ${httpStatus}` }) + localLogger.debug({ context: 'createChallenge', message: `Response Body: ${JSON.stringify(challenge)}` }) + return challenge +} + +/** + * Update a challenge + * + * @param {String} challengeId id of the challenge + * @param {Object} data challenge data + * @param {String} token m2m token + * @returns {Object} the challenge updated + */ +async function updateChallenge (challengeId, data, token) { + if (!token) { + token = await getM2MToken() + } + const url = `${config.TC_API}/challenges/${challengeId}` + localLogger.debug({ context: 'updateChallenge', message: `EndPoint: PATCH ${url}` }) + localLogger.debug({ context: 'updateChallenge', message: `Request Body: ${JSON.stringify(data)}` }) + const { body: challenge, status: httpStatus } = await request + .patch(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .send(data) + localLogger.debug({ context: 'updateChallenge', message: `Status Code: ${httpStatus}` }) + localLogger.debug({ context: 'updateChallenge', message: `Response Body: ${JSON.stringify(challenge)}` }) + return challenge +} + +/** + * Create a challenge resource + * + * @param {Object} data resource + * @param {String} token m2m token + * @returns {Object} the resource created + */ +async function createChallengeResource (data, token) { + if (!token) { + token = await getM2MToken() + } + const url = `${config.TC_API}/resources` + localLogger.debug({ context: 'createChallengeResource', message: `EndPoint: POST ${url}` }) + localLogger.debug({ context: 'createChallengeResource', message: `Request Body: ${JSON.stringify(data)}` }) + const { body: resource, status: httpStatus } = await request + .post(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .send(data) + localLogger.debug({ context: 'createChallengeResource', message: `Status Code: ${httpStatus}` }) + localLogger.debug({ context: 'createChallengeResource', message: `Response Body: ${JSON.stringify(resource)}` }) + return resource +} + module.exports = { getParamFromCliArgs, promptUser, @@ -1159,5 +1235,8 @@ module.exports = { createProjectMember, listProjectMembers, listProjectMemberInvites, - deleteProjectMember + deleteProjectMember, + createChallenge, + updateChallenge, + createChallengeResource } diff --git a/src/services/PaymentService.js b/src/services/PaymentService.js new file mode 100644 index 00000000..e7a7d534 --- /dev/null +++ b/src/services/PaymentService.js @@ -0,0 +1,164 @@ +/** + * This service provides operations of PaymentService. + */ + +const _ = require('lodash') +const Joi = require('joi') +const config = require('config') +const helper = require('../common/helper') +const logger = require('../common/logger') +const constants = require('../../app-constants') + +const localLogger = { + debug: (message) => logger.debug({ component: 'PaymentService', context: message.context, message: message.message }), + error: (message) => logger.error({ component: 'PaymentService', context: message.context, message: message.message }), + info: (message) => logger.info({ component: 'PaymentService', context: message.context, message: message.message }) +} + +/** + * Create payment + * @param {Object} options the user who perform this operation + * @param {Object} options.projectId the user who perform this operation + * @param {Object} options.userHandle the user who perform this operation + * @param {Object} options.amount the user who perform this operation + * @param {Object} options.billingAccountId the user who perform this operation + * @param {Object} options.name the user who perform this operation + * @param {Object} options.description the user who perform this operation + * @returns {Object} the completed challenge + */ +async function createPayment (options) { + if (_.isUndefined(options.name)) { + options.name = `TaaS payment for user ${options.userHandle} in project ${options.projectId}` + } + if (_.isUndefined(options.description)) { + options.description = `TaaS payment for user ${options.userHandle} in project ${options.projectId}` + } + localLogger.info({ context: 'createPayment', message: 'generating M2MToken' }) + const token = await helper.getM2MToken() + localLogger.info({ context: 'createPayment', message: 'M2MToken is generated' }) + const challengeId = await createChallenge(options, token) + await addResourceToChallenge(challengeId, options.userHandle, token) + await activateChallenge(challengeId, token) + const completedChallenge = await closeChallenge(challengeId, token) + return completedChallenge +} + +createPayment.schema = Joi.object().keys({ + options: Joi.object().keys({ + projectId: Joi.number().integer().required(), + userHandle: Joi.string().required(), + amount: Joi.number().positive().required(), + billingAccountId: Joi.number().allow(null), + name: Joi.string(), + description: Joi.string() + }).required() +}).required() + +/** + * Create a new challenge. + * @param {Object} challenge the challenge to create + * @param {String} token m2m token + * @returns {Number} the created challenge id + */ +async function createChallenge (challenge, token) { + localLogger.info({ context: 'createChallenge', message: 'creating new challenge' }) + const body = { + status: constants.ChallengeStatus.DRAFT, + projectId: challenge.projectId, + name: challenge.name, + description: challenge.description, + descriptionFormat: 'markdown', + typeId: config.TYPE_ID_TASK, + trackId: config.DEFAULT_TRACK_ID, + timelineTemplateId: config.DEFAULT_TIMELINE_TEMPLATE_ID, + prizeSets: [{ + type: 'placement', + prizes: [{ type: 'USD', value: challenge.amount }] + }], + legacy: { + pureV5Task: true + }, + tags: ['Other'], + startDate: new Date(), + billing: { billingAccountId: challenge.billingAccountId } + } + try { + const response = await helper.createChallenge(body, token) + const challengeId = _.get(response, 'id') + localLogger.info({ context: 'createChallenge', message: `Challenge with id ${challengeId} is created` }) + return challengeId + } catch (err) { + localLogger.error({ context: 'createChallenge', message: `Status Code: ${err.status}` }) + localLogger.error({ context: 'createChallenge', message: err.response.text }) + throw err + } +} + +/** + * adds the resource to the topcoder challenge + * @param {String} id the challenge id + * @param {String} handle the user handle to add + * @param {String} token m2m token + */ +async function addResourceToChallenge (id, handle, token) { + localLogger.info({ context: 'addResourceToChallenge', message: `adding resource to challenge ${id}` }) + try { + const body = { + challengeId: id, + memberHandle: handle, + roleId: config.ROLE_ID_SUBMITTER + } + await helper.createChallengeResource(body, token) + localLogger.info({ context: 'addResourceToChallenge', message: `${handle} added to challenge ${id}` }) + } catch (err) { + localLogger.error({ context: 'addResourceToChallenge', message: `Status Code: ${err.status}` }) + localLogger.error({ context: 'addResourceToChallenge', message: err.response.text }) + throw err + } +} + +/** + * activates the topcoder challenge + * @param {String} id the challenge id + * @param {String} token m2m token + */ +async function activateChallenge (id, token) { + localLogger.info({ context: 'activateChallenge', message: `Activating challenge ${id}` }) + try { + const body = { + status: constants.ChallengeStatus.ACTIVE + } + await helper.updateChallenge(id, body, token) + localLogger.info({ context: 'activateChallenge', message: `Challenge ${id} is activated successfully.` }) + } catch (err) { + localLogger.error({ context: 'activateChallenge', message: `Status Code: ${err.status}` }) + localLogger.error({ context: 'activateChallenge', message: err.response.text }) + throw err + } +} + +/** + * closes the topcoder challenge + * @param {String} id the challenge id + * @param {String} token m2m token + * @returns {Object} the closed challenge + */ +async function closeChallenge (id, token) { + localLogger.info({ context: 'closeChallenge', message: `Closing challenge ${id}` }) + try { + const body = { + status: constants.ChallengeStatus.COMPLETED + } + const response = await helper.updateChallenge(id, body, token) + localLogger.info({ context: 'closeChallenge', message: `Challenge ${id} is closed successfully.` }) + return response + } catch (err) { + localLogger.error({ context: 'closeChallenge', message: `Status Code: ${err.status}` }) + localLogger.error({ context: 'closeChallenge', message: err.response.text }) + throw err + } +} + +module.exports = { + createPayment +}