diff --git a/config/default.js b/config/default.js index dc7432d..d67846d 100644 --- a/config/default.js +++ b/config/default.js @@ -77,8 +77,8 @@ module.exports = { WEBSITE_SECURE: process.env.WEBSITE_SECURE || 'https://topcoderx.topcoder-dev.com', ROLE_ID_COPILOT: process.env.ROLE_ID_COPILOT || 'cfe12b3f-2a24-4639-9d8b-ec86726f76bd', - ROLE_ID_SUBMITTER: process.env.ROLE_ID_SUBMITTER || '732339e7-8e30-49d7-9198-cccf9451e221', + ROLE_ID_SUBMITTER: process.env.ROLE_ID_SUBMITTER || '0bc7e20e-cf08-4051-b6e5-421d69e492ac', TYPE_ID_FIRST2FINISH: process.env.TYPE_ID_FIRST2FINISH || '927abff4-7af9-4145-8ba1-577c16e64e2e', - DEFAULT_TIMELINE_TEMPLATE_ID: process.env.DEFAULT_TIMELINE_TEMPLATE_ID || '7ebf1c69-f62f-4d3a-bdfb-fe9ddb56861c', - DEFAULT_TRACK_ID : process.env.DEFAULT_TIMELINE_TEMPLATE_ID || '9b6fc876-f4d9-4ccb-9dfd-419247628825' + DEFAULT_TIMELINE_TEMPLATE_ID: process.env.DEFAULT_TIMELINE_TEMPLATE_ID || '6969125a-a12f-4b89-8de6-e66b0056f36b', + DEFAULT_TRACK_ID: process.env.DEFAULT_TIMELINE_TEMPLATE_ID || '9b6fc876-f4d9-4ccb-9dfd-419247628825' }; diff --git a/constants.js b/constants.js index c585411..54b50d0 100644 --- a/constants.js +++ b/constants.js @@ -31,9 +31,22 @@ const CHALLENGE_STATUS = { const SERVICE_ERROR_STATUS = 500; +// issue status +const ISSUE_STATUS = { + CHALLENGE_CANCELLED: 'challenge_cancelled', + CHALLENGE_CREATION_PENDING: 'challenge_creation_pending', + CHALLENGE_CREATION_SUCCESSFUL: 'challenge_creation_successful', + CHALLENGE_CREATION_FAILED: 'challenge_creation_failed', + CHALLENGE_CREATION_RETRIED: 'challenge_creation_retried', + CHALLENGE_PAYMENT_SUCCESSFUL: 'challenge_payment_successful', + CHALLENGE_PAYMENT_PENDING: 'challenge_payment_pending', + CHALLENGE_PAYMENT_FAILED: 'challenge_payment_failed' +}; + module.exports = { USER_ROLES, USER_TYPES, SERVICE_ERROR_STATUS, - CHALLENGE_STATUS + CHALLENGE_STATUS, + ISSUE_STATUS }; diff --git a/services/CopilotPaymentService.js b/services/CopilotPaymentService.js index 154268c..0a37545 100644 --- a/services/CopilotPaymentService.js +++ b/services/CopilotPaymentService.js @@ -63,14 +63,14 @@ function updateChallengeDetails(payments) { */ async function getExistingChallengeIdIfExists(event, dbPayment) { // check if there is existing active challenge associated with this project - let existingPayments = await dbHelper.scan(models.CopilotPayment, { + const existingPayments = await dbHelper.scan(models.CopilotPayment, { project: {eq: dbPayment.project}, username: {eq: event.project.copilot}, closed: {eq: 'false'} }); - const payment = _.find(existingPayments, x => x.challengeUUID); - + const payment = _.find(existingPayments, (x) => x.challengeUUID); + // if no existing challenge found then it will be created by processor if (payment) { // update db payment @@ -286,7 +286,7 @@ async function handlePaymentUpdates(event) { }, ExpressionAttributeValues: filterValues }); - + if (dbPayments) { const challengeIds = _(dbPayments).map('challengeUUID').uniq().filter(_.isString) .value(); diff --git a/services/GithubService.js b/services/GithubService.js index b436225..cf7afd0 100644 --- a/services/GithubService.js +++ b/services/GithubService.js @@ -86,7 +86,7 @@ async function _removeAssignees(github, owner, repo, number, assignees) { * @returns {string} username if found */ async function _getUsernameById(github, id) { - const user = await github.users.getById({ id }); + const user = await github.users.getById({id}); return user ? user.data.login : null; } @@ -98,11 +98,11 @@ async function _getUsernameById(github, id) { * @param {string} title new title */ async function updateIssue(copilot, repoFullName, number, title) { - Joi.attempt({ copilot, repoFullName, number, title }, updateIssue.schema); + Joi.attempt({copilot, repoFullName, number, title}, updateIssue.schema); const github = await _authenticate(copilot.accessToken); - const { owner, repo } = _parseRepoUrl(repoFullName); + const {owner, repo} = _parseRepoUrl(repoFullName); try { - await github.issues.edit({ owner, repo, number, title }); + await github.issues.edit({owner, repo, number, title}); } catch (err) { throw errors.convertGitHubError(err, 'Error occurred during updating issue.'); } @@ -124,17 +124,17 @@ updateIssue.schema = { * @param {string} user the user login of assignee */ async function assignUser(copilot, repoFullName, number, user) { - Joi.attempt({ copilot, repoFullName, number, user }, assignUser.schema); + Joi.attempt({copilot, repoFullName, number, user}, assignUser.schema); const github = await _authenticate(copilot.accessToken); - const { owner, repo } = _parseRepoUrl(repoFullName); + const {owner, repo} = _parseRepoUrl(repoFullName); try { - const issue = await github.issues.get({ owner, repo, number }); + const issue = await github.issues.get({owner, repo, number}); const oldAssignees = _(issue.data.assignees).map('login').without(user).value(); if (oldAssignees && oldAssignees.length > 0) { await _removeAssignees(github, owner, repo, number, oldAssignees); } - await github.issues.addAssigneesToIssue({ owner, repo, number, assignees: [user] }); + await github.issues.addAssigneesToIssue({owner, repo, number, assignees: [user]}); } catch (err) { throw errors.convertGitHubError(err, 'Error occurred during assigning issue user.'); } @@ -156,10 +156,10 @@ assignUser.schema = { * @param {string} user the user login of assignee */ async function removeAssign(copilot, repoFullName, number, user) { - Joi.attempt({ copilot, repoFullName, number, user }, removeAssign.schema); + Joi.attempt({copilot, repoFullName, number, user}, removeAssign.schema); const github = await _authenticate(copilot.accessToken); - const { owner, repo } = _parseRepoUrl(repoFullName); + const {owner, repo} = _parseRepoUrl(repoFullName); await _removeAssignees(github, owner, repo, number, [user]); logger.debug(`Github user ${user} is unassigned from issue number ${number}`); } @@ -174,12 +174,12 @@ removeAssign.schema = assignUser.schema; * @param {string} body the comment body text */ async function createComment(copilot, repoFullName, number, body) { - Joi.attempt({ copilot, repoFullName, number, body }, createComment.schema); + Joi.attempt({copilot, repoFullName, number, body}, createComment.schema); const github = await _authenticate(copilot.accessToken); - const { owner, repo } = _parseRepoUrl(repoFullName); + const {owner, repo} = _parseRepoUrl(repoFullName); try { body = helper.prepareAutomatedComment(body, copilot); - await github.issues.createComment({ owner, repo, number, body }); + await github.issues.createComment({owner, repo, number, body}); } catch (err) { throw errors.convertGitHubError(err, 'Error occurred during creating comment on issue.'); } @@ -200,7 +200,7 @@ createComment.schema = { * @returns {string} the username if found else null */ async function getUsernameById(copilot, userId) { - Joi.attempt({ copilot, userId }, getUsernameById.schema); + Joi.attempt({copilot, userId}, getUsernameById.schema); const github = await _authenticate(copilot.accessToken); const login = await _getUsernameById(github, userId); return login; @@ -218,9 +218,9 @@ getUsernameById.schema = { * @returns {Number} the user id if found else null */ async function getUserIdByLogin(copilot, login) { - Joi.attempt({ copilot, login }, getUserIdByLogin.schema); + Joi.attempt({copilot, login}, getUserIdByLogin.schema); const github = await _authenticate(copilot.accessToken); - const user = await github.users.getForUser({ username: login }); + const user = await github.users.getForUser({username: login}); return user.data ? user.data.id : null; } @@ -243,11 +243,11 @@ getUserIdByLogin.schema = { async function markIssueAsPaid(copilot, repoFullName, number, challengeUUID, existLabels, winner, createCopilotPayments) { // eslint-disable-line max-params Joi.attempt({copilot, repoFullName, number, challengeUUID, existLabels, winner, createCopilotPayments}, markIssueAsPaid.schema); const github = await _authenticate(copilot.accessToken); - const { owner, repo } = _parseRepoUrl(repoFullName); + const {owner, repo} = _parseRepoUrl(repoFullName); const labels = _(existLabels).filter((i) => i !== config.FIX_ACCEPTED_ISSUE_LABEL) .push(config.FIX_ACCEPTED_ISSUE_LABEL, config.PAID_ISSUE_LABEL).value(); try { - await github.issues.edit({ owner, repo, number, labels }); + await github.issues.edit({owner, repo, number, labels}); let commentMessage = ''; commentMessage += `Payment task has been updated: ${config.TC_URL}/challenges/${challengeUUID}\n`; commentMessage += '*Payments Complete*\n'; @@ -283,11 +283,11 @@ markIssueAsPaid.schema = { * @param {string} state new state */ async function changeState(copilot, repoFullName, number, state) { - Joi.attempt({ copilot, repoFullName, number, state }, changeState.schema); + Joi.attempt({copilot, repoFullName, number, state}, changeState.schema); const github = await _authenticate(copilot.accessToken); - const { owner, repo } = _parseRepoUrl(repoFullName); + const {owner, repo} = _parseRepoUrl(repoFullName); try { - await github.issues.edit({ owner, repo, number, state }); + await github.issues.edit({owner, repo, number, state}); } catch (err) { throw errors.convertGitHubError(err, 'Error occurred during updating status of issue.'); } @@ -309,11 +309,11 @@ changeState.schema = { * @param {Number} labels the challenge id */ async function addLabels(copilot, repoFullName, number, labels) { - Joi.attempt({ copilot, repoFullName, number, labels }, addLabels.schema); + Joi.attempt({copilot, repoFullName, number, labels}, addLabels.schema); const github = await _authenticate(copilot.accessToken); - const { owner, repo } = _parseRepoUrl(repoFullName); + const {owner, repo} = _parseRepoUrl(repoFullName); try { - await github.issues.edit({ owner, repo, number, labels }); + await github.issues.edit({owner, repo, number, labels}); } catch (err) { throw errors.convertGitHubError(err, 'Error occurred during adding label in issue.'); } diff --git a/services/IssueService.js b/services/IssueService.js index c24aa1f..f65c4b2 100755 --- a/services/IssueService.js +++ b/services/IssueService.js @@ -77,22 +77,27 @@ async function ensureChallengeExists(event, issue, create = true) { logger.debugWithContext(`DB Issue number: ${issue.number}`, event, issue); logger.debugWithContext(`DB Issue provider: ${issue.provider}`, event, issue); logger.debugWithContext(`DB Issue repository: ${issue.repositoryId}`, event, issue); - if (dbIssue && dbIssue.status === 'challenge_creation_pending') { + + if (dbIssue && dbIssue.status === constants.ISSUE_STATUS.CHALLENGE_CREATION_PENDING) { logger.debugWithContext('dbIssue is PENDING', event, issue); throw errors.internalDependencyError(`Challenge for the updated issue ${issue.number} is creating, rescheduling this event`); } - if (dbIssue && dbIssue.status === 'challenge_creation_failed') { + const hasOpenForPickupLabel = _(issue.labels).includes(config.OPEN_FOR_PICKUP_ISSUE_LABEL); + if (dbIssue && dbIssue.status === constants.ISSUE_STATUS.CHALLENGE_CREATION_FAILED && hasOpenForPickupLabel) { // remove issue from db await dbHelper.removeIssue(models.Issue, issue.repositoryId, issue.number, issue.provider); dbIssue = null; } + if (dbIssue && dbIssue.status === constants.ISSUE_STATUS.CHALLENGE_CANCELLED) { + dbIssue = null; + } if (!dbIssue && create) { logger.debugWithContext('dbIssue is NULL, process to create new record and challenge', event, issue); await handleIssueCreate(event, issue, true); dbIssue = await dbHelper.queryOneIssue(models.Issue, issue.repositoryId, issue.number, issue.provider); - logger.debugWithContext(`dbIssue is CREATED ${dbIssue ? 'Succesfully' : 'Failed'}`, event, issue); + logger.debugWithContext(`dbIssue is CREATED ${dbIssue ? 'Successfully' : 'Failed'}`, event, issue); } return dbIssue; } @@ -384,19 +389,19 @@ async function handleIssueClose(event, issue) { // eslint-disable-line event.dbIssue = dbIssue; // if the issue has payment success or payment pending status, we'll ignore this process. - if (dbIssue && dbIssue.status === 'challenge_payment_successful') { + if (dbIssue && dbIssue.status === constants.ISSUE_STATUS.CHALLENGE_PAYMENT_SUCCESSFUL) { logger.debugWithContext('Ignoring close issue processing. The issue has challenge_payment_successful.', event, issue); return; } - if (dbIssue && dbIssue.status === 'challenge_payment_pending') { + if (dbIssue && dbIssue.status === constants.ISSUE_STATUS.CHALLENGE_PAYMENT_PENDING) { logger.debugWithContext('Ignoring close issue processing. The issue has challenge_payment_pending.', event, issue); return; } if (!event.paymentSuccessful) { let closeChallenge = false; - // if issue is closed without Fix accepted label - if (!_.includes(event.data.issue.labels, config.FIX_ACCEPTED_ISSUE_LABEL) || _.includes(event.data.issue.labels, config.CANCELED_ISSUE_LABEL)) { + // if issue is closed without Fix accepted and cancel label + if (!_.includes(event.data.issue.labels, config.FIX_ACCEPTED_ISSUE_LABEL) && !_.includes(event.data.issue.labels, config.CANCELED_ISSUE_LABEL)) { logger.debugWithContext(`This issue ${issue.number} is closed without fix accepted label.`, event, issue); let comment = 'This ticket was not processed for payment. If you would like to process it for payment,'; comment += ' please reopen it, add the ```' + config.FIX_ACCEPTED_ISSUE_LABEL + '``` label, and then close it again';// eslint-disable-line @@ -404,6 +409,19 @@ async function handleIssueClose(event, issue) { // eslint-disable-line closeChallenge = true; } + // if issue is close with cancelled label + if (_.includes(event.data.issue.labels, config.CANCELED_ISSUE_LABEL)) { + const comment = `Challenge ${dbIssue.challengeUUID} has been cancelled`; + await topcoderApiHelper.cancelPrivateContent(dbIssue.challengeUUID); + await gitHelper.createComment(event, issue.number, comment); + // update the issue status to payment pending to prevent double processing. + await dbHelper.update(models.Issue, dbIssue.id, { + status: constants.ISSUE_STATUS.CHALLENGE_CANCELLED, + updatedAt: new Date() + }); + closeChallenge = true; + } + if (issue.prizes[0] === 0) { closeChallenge = true; } @@ -437,7 +455,7 @@ async function handleIssueClose(event, issue) { // eslint-disable-line // update the issue status to payment pending to prevent double processing. await dbHelper.update(models.Issue, dbIssue.id, { - status: 'challenge_payment_pending', + status: constants.ISSUE_STATUS.CHALLENGE_PAYMENT_PENDING, updatedAt: new Date() }); @@ -503,7 +521,7 @@ async function handleIssueClose(event, issue) { // eslint-disable-line // update the issue status to payment failed if (!event.paymentSuccessful && dbIssue && dbIssue.id) { await dbHelper.update(models.Issue, dbIssue.id, { - status: 'challenge_payment_failed', + status: constants.ISSUE_STATUS.CHALLENGE_PAYMENT_FAILED, updatedAt: new Date() }); } @@ -520,7 +538,7 @@ async function handleIssueClose(event, issue) { // eslint-disable-line .value(); dbIssue = await dbHelper.update(models.Issue, dbIssue.id, { labels, - status: 'challenge_payment_successful', + status: constants.ISSUE_STATUS.CHALLENGE_PAYMENT_SUCCESSFUL, updatedAt: new Date() }); await gitHelper.markIssueAsPaid(event, issue.number, dbIssue.challengeUUID, labels, event.assigneeMember.topcoderUsername, @@ -552,7 +570,7 @@ async function handleIssueCreate(event, issue, forceAssign = false) { // Check if duplicated let dbIssue = await dbHelper.queryOneIssue(models.Issue, issue.repositoryId, issue.number, issue.provider); - if (dbIssue) { + if (dbIssue && dbIssue.status !== constants.ISSUE_STATUS.CHALLENGE_CANCELLED) { throw new Error( `Issue ${issue.number} is already in ${dbIssue.status}`); } @@ -573,9 +591,17 @@ async function handleIssueCreate(event, issue, forceAssign = false) { // create issue with challenge creation pending const issueObject = _.assign({}, _.omit(issue, 'assignee'), { id: helper.generateIdentifier(), - status: 'challenge_creation_pending' + status: constants.ISSUE_STATUS.CHALLENGE_CREATION_PENDING }); - dbIssue = await dbHelper.create(models.Issue, issueObject); + + if (!dbIssue) { + dbIssue = await dbHelper.create(models.Issue, issueObject); + } else if (dbIssue && dbIssue.status !== 'challenge_cancelled') { + await dbHelper.update(models.Issue, dbIssue.id, { + status: constants.ISSUE_STATUS.CHALLENGE_CREATION_PENDING, + updatedAt: new Date() + }); + } const projectId = project.tcDirectId; @@ -593,7 +619,7 @@ async function handleIssueCreate(event, issue, forceAssign = false) { // update db payment await dbHelper.update(models.Issue, dbIssue.id, { challengeUUID: issue.challengeUUID, - status: 'challenge_creation_successful', + status: constants.ISSUE_STATUS.CHALLENGE_CREATION_SUCCESSFUL, updatedAt: new Date() }); } catch (e) { diff --git a/utils/topcoder-api-helper.js b/utils/topcoder-api-helper.js index 66f2210..81cb404 100644 --- a/utils/topcoder-api-helper.js +++ b/utils/topcoder-api-helper.js @@ -386,7 +386,7 @@ async function cancelPrivateContent(id) { loggerFile.info(`EndPoint: PATCH /challenges/${id}, PATCH parameters: { status: '${constants.CHALLENGE_STATUS.CANCELED}' }, Status Code:null, Error: 'Failed to cancel challenge.', Details: ${circularJSON.stringify(err)}`); - throw errors.convertTopcoderApiError(err, 'Failed to activate challenge.'); + throw errors.convertTopcoderApiError(err, 'Failed to cancel challenge.'); } } @@ -426,9 +426,9 @@ async function removeResourceToChallenge(id, handle, roleId) { DELETE parameters: null, Status Code:${statusCode}, Response:${circularJSON.stringify(response.data)}`); } catch (err) { loggerFile.info(`EndPoint: DELETE /resources, DELETE parameters: null, Status Code:null, - Error: 'Failed to add resource to the challenge.', Details: ${circularJSON.stringify(err)}`); + Error: 'Failed to remove resource from the challenge.', Details: ${circularJSON.stringify(err)}`); logger.error(`Response Data: ${JSON.stringify(err.response.data)}`); - throw errors.convertTopcoderApiError(err, 'Failed to add resource to the challenge.'); + throw errors.convertTopcoderApiError(err, 'Failed to remove resource from the challenge.'); } }