diff --git a/README.md b/README.md index b413b1b..8eee59e 100755 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ The following config parameters are supported, they are defined in `config/defau |AUTH0_CLIENT_ID| The Auth0 ClientID for generating Machine-to-machine token || |AUTH0_CLIENT_SECRET| The Auth0 Client Secret for generating Machine-to-machine token || |ROLE_ID_COPILOT| The registered role id of copilot || +|ROLE_ID_ITERATIVE_REVIEWER| The registered role id of iterative reviewer || |ROLE_ID_SUBMITTER| The registered role id of submitter || |TYPE_ID_TASK| The registered type id of a task || |DEFAULT_TIMELINE_TEMPLATE_ID| The default timeline template id || diff --git a/config/default.js b/config/default.js index 380ea10..2256a15 100644 --- a/config/default.js +++ b/config/default.js @@ -64,6 +64,7 @@ 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_ITERATIVE_REVIEWER: process.env.ROLE_ID_ITERATIVE_REVIEWER || 'f6df7212-b9d6-4193-bfb1-b383586fce63', 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', diff --git a/configuration.md b/configuration.md index 3937326..5ca0e4f 100644 --- a/configuration.md +++ b/configuration.md @@ -35,6 +35,7 @@ The following config parameters are supported, they are defined in `config/defau |AUTH0_CLIENT_ID| The Auth0 ClientID for generating Machine-to-machine token || |AUTH0_CLIENT_SECRET| The Auth0 Client Secret for generating Machine-to-machine token || |ROLE_ID_COPILOT| The registered role id of copilot || +|ROLE_ID_ITERATIVE_REVIEWER| The registered role id of iterative reviewer || |ROLE_ID_SUBMITTER| The registered role id of submitter || |TYPE_ID_TASK| The registered type id of a task || |DEFAULT_TIMELINE_TEMPLATE_ID| The default timeline template id || diff --git a/models/CopilotPayment.js b/models/CopilotPayment.js index d33a851..46c8674 100644 --- a/models/CopilotPayment.js +++ b/models/CopilotPayment.js @@ -21,64 +21,29 @@ const schema = new Schema({ required: true }, project: { - type: String, - index: { - global: true, - rangeKey: 'id', - project: true, - name: 'ProjectIndex' - } + type: String }, amount: {type: Number, required: true}, description: {type: String, required: true}, challengeId: { type: Number, - required: false, - index: { - global: true, - rangeKey: 'id', - project: true, - name: 'ChallengeIdIndex' - } + required: false }, challengeUUID: { type: String, - required: false, - index: { - global: true, - project: true, - name: 'ChallengeUUIdIndex' - } + required: false }, closed: { type: String, required: true, - default: 'false', - index: { - global: true, - rangeKey: 'id', - project: true, - name: 'ClosedIndex' - } + default: 'false' }, username: { type: String, - required: true, - index: { - global: true, - rangeKey: 'id', - project: true, - name: 'UsernameIndex' - } + required: true }, status: { - type: String, - index: { - global: true, - rangeKey: 'id', - project: true, - name: 'StatusIndex' - } + type: String } }); diff --git a/models/Issue.js b/models/Issue.js index 47b85b8..082b3c8 100644 --- a/models/Issue.js +++ b/models/Issue.js @@ -17,33 +17,21 @@ const schema = new Schema({ // From the receiver service number: { type: Number, - required: true, - index: { - global: true, - rangeKey: 'id', - project: true, - name: 'NumberIndex' - } + required: true }, title: {type: String, required: true}, body: {type: String}, prizes: {type: [Number], required: true}, // extracted from title provider: { type: String, - required: true, - index: { - global: true, - rangeKey: 'id', - project: true, - name: 'ProviderIndex' - } + required: true }, // github or gitlab repositoryId: { type: Number, required: true, index: { global: true, - rangeKey: 'id', + rangeKey: 'number', project: true, name: 'RepositoryIdIndex' } diff --git a/models/Project.js b/models/Project.js index e66602f..b59efdf 100755 --- a/models/Project.js +++ b/models/Project.js @@ -21,15 +21,18 @@ const schema = new Schema({ title: {type: String, required: true}, tcDirectId: { type: Number, + required: true + }, + repoUrl: { + type: String, required: true, index: { global: true, - rangeKey: 'id', + rangeKey: 'archived', project: true, - name: 'TcDirectIdIndex' + name: 'RepoUrlIndex' } }, - repoUrl: {type: String, required: true}, repoId: {type: String, required: false}, rocketChatWebhook: {type: String, required: false}, rocketChatChannelName: {type: String, required: false}, diff --git a/models/User.js b/models/User.js index 50b8440..dc3ca69 100755 --- a/models/User.js +++ b/models/User.js @@ -20,13 +20,7 @@ const schema = new Schema({ }, userProviderId: { type: Number, - required: true, - index: { - global: true, - rangeKey: 'id', - project: true, - name: 'UsesProviderIdIndex' - } + required: true }, userProviderIdStr: { type: String, @@ -37,7 +31,7 @@ const schema = new Schema({ required: true, index: { global: true, - rangeKey: 'id', + rangeKey: 'type', project: true, name: 'UsernameIndex' } @@ -45,24 +39,12 @@ const schema = new Schema({ role: { type: String, required: true, - enum: _.values(constants.USER_ROLES), - index: { - global: true, - project: true, - name: 'RoleIndex', - rangeKey: 'id' - } + enum: _.values(constants.USER_ROLES) }, type: { type: String, required: true, - enum: _.values(constants.USER_TYPES), - index: { - global: true, - rangeKey: 'id', - name: 'TypeIndex', - project: true - } + enum: _.values(constants.USER_TYPES) }, // gitlab token data accessToken: {type: String, required: false}, diff --git a/models/UserMapping.js b/models/UserMapping.js index 1662597..d6484f1 100644 --- a/models/UserMapping.js +++ b/models/UserMapping.js @@ -23,10 +23,42 @@ const schema = new Schema({ name: 'TopcoderUsernameIndex' } }, - githubUsername: String, - gitlabUsername: String, - githubUserId: Number, - gitlabUserId: Number + githubUsername: { + type: String, + index: { + global: true, + project: true, + rangKey: 'id', + name: 'GithubUsernameIndex' + } + }, + gitlabUsername: { + type: String, + index: { + global: true, + project: true, + rangKey: 'id', + name: 'GitlabUsernameIndex' + } + }, + githubUserId: { + type: Number, + index: { + global: true, + project: true, + rangKey: 'id', + name: 'GithubUserIdIndex' + } + }, + gitlabUserId: { + type: Number, + index: { + global: true, + project: true, + rangKey: 'id', + name: 'GitlabUserIdIndex' + } + } }); module.exports = schema; diff --git a/services/EventService.js b/services/EventService.js index c42b759..d2cbece 100644 --- a/services/EventService.js +++ b/services/EventService.js @@ -50,7 +50,7 @@ async function handleEventGracefully(event, data, err) { // reschedule event if (event.retryCount < config.RETRY_COUNT) { logger.debug('Scheduling event for next retry'); - const newEvent = { ...event }; + const newEvent = {...event}; newEvent.retryCount += 1; delete newEvent.copilot; const timeoutKey = setTimeout(async () => { @@ -79,7 +79,7 @@ async function handleEventGracefully(event, data, err) { } else if (event.event === 'issue.created') { if (err.name === 'ProcessorError' && err.statusCode && err.message) { // comment for challenge creation failed - comment = `[${err.statusCode}]: ${err.message}` + comment = `[${err.statusCode}]: ${err.message}`; } else { // comment for challenge creation failed comment = 'The challenge creation on the Topcoder platform failed. Please contact support to try again'; @@ -87,9 +87,7 @@ async function handleEventGracefully(event, data, err) { } else if (event.event === 'copilotPayment.add') { // comment for copilot payment challenge create failed comment = 'The copilot payment challenge creation on the Topcoder platform failed. Please contact support to try again'; - await dbHelper.remove(models.CopilotPayment, { - id: { eq: data.id } - }); + await dbHelper.removeCopilotPayment(models.CopilotPayment, data.id); // we dont need to put comment for copilot payment return; } diff --git a/services/GithubService.js b/services/GithubService.js index cf7afd0..64aec0e 100644 --- a/services/GithubService.js +++ b/services/GithubService.js @@ -33,7 +33,7 @@ function _parseRepoUrl(fullName) { const results = fullName.split('/'); const repo = results[results.length - 1]; const owner = _(results).slice(0, results.length - 1).join('/'); - return { owner, repo }; + return {owner, repo}; } /** diff --git a/services/IssueService.js b/services/IssueService.js index c93007b..09a52f8 100755 --- a/services/IssueService.js +++ b/services/IssueService.js @@ -108,10 +108,7 @@ async function ensureChallengeExists(event, issue, create = true) { */ async function getProjectDetail(event) { const fullRepoUrl = gitHelper.getFullRepoUrl(event); - const project = await dbHelper.scanOne(models.Project, { - repoUrl: fullRepoUrl, - archived: 'false' - }); + const project = await dbHelper.queryOneActiveProject(models.Project, fullRepoUrl); return project; } @@ -202,7 +199,11 @@ async function handleIssueAssignment(event, issue, force = false) { } return; } - + // if the issue has payment success we'll ignore this process. + if (dbIssue.status === constants.ISSUE_STATUS.CHALLENGE_PAYMENT_SUCCESSFUL) { + logger.debugWithContext('Ignoring this issue processing. The issue has challenge_payment_successful.', event, issue); + return; + } // Handle multiple assignees. TC-X allows only one assignee. if (event.data.issue.assignees && event.data.issue.assignees.length > 1) { const comment = 'Topcoder-X only supports a single assignee on a ticket to avoid issues with payment'; @@ -332,7 +333,11 @@ async function handleIssueUpdate(event, issue) { } return; } - + // if the issue has payment success we'll ignore this process. + if (dbIssue.status === constants.ISSUE_STATUS.CHALLENGE_PAYMENT_SUCCESSFUL) { + logger.debugWithContext('Ignoring this issue processing. The issue has challenge_payment_successful.', event, issue); + return; + } if (dbIssue.title === issue.title && dbIssue.body === issue.body && dbIssue.prizes.length === issue.prizes.length && @@ -410,7 +415,7 @@ async function handleIssueClose(event, issue) { // eslint-disable-line 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 await gitHelper.createComment(event, issue.number, comment); - closeChallenge = true; + return; } // if issue is close with cancelled label @@ -493,8 +498,8 @@ async function handleIssueClose(event, issue) { // eslint-disable-line event.createCopilotPayments = createCopilotPayments; if (createCopilotPayments) { - logger.debugWithContext(`Setting copilot payment`); - + logger.debugWithContext('Setting copilot payment'); + const updateBody = { prizeSets: [{ type: 'placement', @@ -504,13 +509,11 @@ async function handleIssueClose(event, issue) { // eslint-disable-line type: 'copilot', prizes: [{type: 'USD', value: 40}] } - ] + ] }; await topcoderApiHelper.updateChallenge(dbIssue.challengeUUID, updateBody); - - } - else { - logger.debugWithContext('Create copilot payments is unchecked on the Topcoder-X project setup, so skipping', event, issue); + } else { + logger.debugWithContext('Create copilot payments is unchecked on the Topcoder-X project setup, so skipping', event, issue); } logger.debugWithContext(`Getting the topcoder member ID for member name: ${assigneeMember.topcoderUsername}`, event, issue); @@ -526,11 +529,12 @@ async function handleIssueClose(event, issue) { // eslint-disable-line } // activate challenge + if (challenge.status === 'Draft') { await topcoderApiHelper.activateChallenge(dbIssue.challengeUUID); //HACK - sleep 30 seconds so the legacy processor has time to "catch up" - logger.debugWithContext('Sleeping for 30 seconds after activation so everything propagates...', event, issue); - await new Promise(resolve => setTimeout(resolve, 30000)); + // logger.debugWithContext('Sleeping for 1 seconds after activation so everything propagates...', event, issue); + // await new Promise(resolve => setTimeout(resolve, 1000)); } logger.debugWithContext(`Closing challenge with winner ${assigneeMember.topcoderUsername}(${winnerId})`, event, issue); @@ -643,11 +647,11 @@ async function handleIssueCreate(event, issue, forceAssign = false) { status: constants.ISSUE_STATUS.CHALLENGE_CREATION_SUCCESSFUL, updatedAt: new Date() }); - + logger.debugWithContext(`Adding copilot to issue: ${event.copilot.topcoderUsername}`, event, issue); // get copilot tc user id await topcoderApiHelper.addResourceToChallenge(issue.challengeUUID, event.copilot.topcoderUsername, config.ROLE_ID_COPILOT); - + await topcoderApiHelper.addResourceToChallenge(issue.challengeUUID, event.copilot.topcoderUsername, config.ROLE_ID_ITERATIVE_REVIEWER); } catch (e) { logger.error(`Challenge creation failure: ${e}`); delete issueCreationLock[creationLockKey]; @@ -697,6 +701,11 @@ async function handleIssueLabelUpdated(event, issue) { logger.debugWithContext('DB record not found. Issue label update ignored.', event, issue); return; } + // if the issue has payment success we'll ignore this process. + if (dbIssue.status === constants.ISSUE_STATUS.CHALLENGE_PAYMENT_SUCCESSFUL) { + logger.debugWithContext('Ignoring this issue processing. The issue has challenge_payment_successful.', event, issue); + return; + } await dbHelper.update(models.Issue, dbIssue.id, { labels: issue.labels, updatedAt: new Date() @@ -719,6 +728,11 @@ async function handleIssueUnAssignment(event, issue) { // Ignore it. return; } + // if the issue has payment success we'll ignore this process. + if (dbIssue.status === constants.ISSUE_STATUS.CHALLENGE_PAYMENT_SUCCESSFUL) { + logger.debugWithContext('Ignoring this issue processing. The issue has challenge_payment_successful.', event, issue); + return; + } if (dbIssue.assignee) { const assigneeUserId = await gitHelper.getUserIdByLogin(event, dbIssue.assignee); if (!assigneeUserId) { @@ -860,10 +874,7 @@ async function process(event) { const fullRepoUrl = gitHelper.getFullRepoUrl(event); event.data.repository.repoUrl = fullRepoUrl; - const project = await dbHelper.scanOne(models.Project, { - repoUrl: fullRepoUrl, - archived: 'false' - }); + const project = await dbHelper.queryOneActiveProject(models.Project, fullRepoUrl); issue.projectId = project.id; diff --git a/services/UserService.js b/services/UserService.js index 5483d56..bed93a0 100755 --- a/services/UserService.js +++ b/services/UserService.js @@ -28,21 +28,20 @@ async function getTCUserName(provider, gitUser) { const criteria = {}; if (_.isNumber(gitUser) || v.isUUID(gitUser)) { if (provider === 'github') { - criteria.githubUserId = gitUser; + return await dbHelper.queryOneUserMappingByGithubUserId(models.UserMapping, gitUser); } else if (provider === 'gitlab') { - criteria.gitlabUserId = gitUser; + return await dbHelper.queryOneUserMappingByGitlabUserId(models.UserMapping, gitUser); } } else if (_.isString(gitUser) || v.isEmail(gitUser)) { if (provider === 'github') { - criteria.githubUsername = gitUser; + return await dbHelper.queryOneUserMappingByGithubUsername(models.UserMapping, gitUser); } else if (provider === 'gitlab') { - criteria.gitlabUsername = gitUser; + return await dbHelper.queryOneUserMappingByGitlabUsername(models.UserMapping, gitUser); } } if (_.isEmpty(criteria)) { throw new Error('Can\'t find the TCUserName. Invalid gitUser.'); } - return await dbHelper.scanOne(models.UserMapping, criteria); } getTCUserName.schema = { @@ -65,9 +64,7 @@ async function getRepositoryCopilotOrOwner(provider, repoFullName) { } else if (provider === 'gitlab') { fullRepoUrl = `${config.GITLAB_API_BASE_URL}/${repoFullName}`; } - const project = await dbHelper.scanOne(models.Project, { - repoUrl: fullRepoUrl - }); + const project = await dbHelper.queryOneActiveProject(models.Project, fullRepoUrl); const hasCopilot = project.copilot !== undefined; // eslint-disable-line no-undefined if (!project || !project.owner) { @@ -75,9 +72,8 @@ async function getRepositoryCopilotOrOwner(provider, repoFullName) { throw new Error(`This repository '${repoFullName}' is not managed by Topcoder X tool.`); } - const userMapping = await dbHelper.scanOne(models.UserMapping, { - topcoderUsername: {eq: hasCopilot ? project.copilot.toLowerCase() : project.owner.toLowerCase()} - }); + const userMapping = await dbHelper.queryOneUserMappingByTCUsername( + models.UserMapping, hasCopilot ? project.copilot.toLowerCase() : project.owner.toLowerCase()); logger.debug('userMapping'); logger.debug(userMapping); @@ -87,11 +83,8 @@ async function getRepositoryCopilotOrOwner(provider, repoFullName) { (provider === 'gitlab' && !userMapping.gitlabUserId)) { throw new Error(`Couldn't find githost username for '${provider}' for this repository '${repoFullName}'.`); } - const user = await dbHelper.scanOne(models.User, { - username: provider === 'github' ? userMapping.githubUsername : // eslint-disable-line no-nested-ternary - userMapping.gitlabUsername, - type: provider - }); + const user = await dbHelper.queryOneUserByType(models.User, + provider === 'github' ? userMapping.githubUsername : userMapping.gitlabUsername, provider); // eslint-disable-line no-nested-ternary if (!user && !hasCopilot) { // throw no copilot is configured diff --git a/utils/db-helper.js b/utils/db-helper.js index ad1bc2f..13051a2 100644 --- a/utils/db-helper.js +++ b/utils/db-helper.js @@ -18,12 +18,12 @@ const logger = require('./logger'); */ async function getById(model, id) { return await new Promise((resolve, reject) => { - model.query('id').eq(id).consistent().exec((err, result) => { + model.queryOne('id').eq(id).consistent().exec((err, result) => { if (err) { return reject(err); } - return resolve(result[0]); + return resolve(result); }); }); } @@ -57,7 +57,7 @@ async function scan(model, scanParams) { async function queryOneIssue(model, repositoryId, number, provider) { return await new Promise((resolve, reject) => { model.query('repositoryId').eq(repositoryId) - .filter('number') + .where('number') .eq(number) .filter('provider') .eq(provider) @@ -74,24 +74,150 @@ async function queryOneIssue(model, repositoryId, number, provider) { } /** - * Get single data by scan parameters - * @param {Object} model The dynamoose model to scan - * @param {Object} scanParams The scan parameters object + * Get single data by query parameters + * @param {Object} model The dynamoose model to query + * @param {String} repoUrl The repository url * @returns {Promise} */ -async function scanOne(model, scanParams) { +async function queryOneActiveProject(model, repoUrl) { return await new Promise((resolve, reject) => { - model.scan(scanParams).consistent().all().exec((err, result) => { + model.query('repoUrl').eq(repoUrl) + .where('archived') + .eq('false') + .all() + .exec((err, result) => { if (err || !result) { - logger.debug(`scanOne. Error. ${err}`); + logger.debug(`queryOneActiveProject. Error. ${err}`); return reject(err); } + return resolve(result.count === 0 ? null : result[0]); + }); + }); +} +/** + * Get single data by query parameters + * @param {Object} model The dynamoose model to query + * @param {String} username The user username + * @param {String} type The type of user + * @returns {Promise} + */ +async function queryOneUserByType(model, username, type) { + return await new Promise((resolve, reject) => { + model.query('username').eq(username) + .where('type') + .eq(type) + .all() + .exec((err, result) => { + if (err || !result) { + logger.debug(`queryOneUserByType. Error. ${err}`); + return reject(err); + } return resolve(result.count === 0 ? null : result[0]); }); }); } +/** + * Get single data by query parameters + * @param {Object} model The dynamoose model to query + * @param {String} tcusername The tc username + * @returns {Promise} + */ +async function queryOneUserMappingByTCUsername(model, tcusername) { + return await new Promise((resolve, reject) => { + model.queryOne('topcoderUsername').eq(tcusername) + .all() + .exec((err, result) => { + if (err || !result) { + logger.debug(`queryOneUserMappingByTCUsername. Error. ${err}`); + return reject(err); + } + return resolve(result); + }); + }); +} + +/** + * Get single data by query parameters + * @param {Object} model The dynamoose model to query + * @param {String} username The username + * @returns {Promise} + */ +async function queryOneUserMappingByGithubUsername(model, username) { + return await new Promise((resolve, reject) => { + model.queryOne('githubUsername').eq(username) + .all() + .exec((err, result) => { + if (err || !result) { + logger.debug(`queryOneUserMappingByGithubUsername. Error. ${err}`); + return reject(err); + } + return resolve(result); + }); + }); +} + +/** + * Get single data by query parameters + * @param {Object} model The dynamoose model to query + * @param {String} username The username + * @returns {Promise} + */ +async function queryOneUserMappingByGitlabUsername(model, username) { + return await new Promise((resolve, reject) => { + model.queryOne('gitlabUsername').eq(username) + .all() + .exec((err, result) => { + if (err || !result) { + logger.debug(`queryOneUserMappingByGitlabUsername. Error. ${err}`); + return reject(err); + } + return resolve(result); + }); + }); +} + +/** + * Get single data by query parameters + * @param {Object} model The dynamoose model to query + * @param {Number} userId The user id + * @returns {Promise} + */ +async function queryOneUserMappingByGithubUserId(model, userId) { + return await new Promise((resolve, reject) => { + model.queryOne('githubUserId').eq(userId) + .all() + .exec((err, result) => { + if (err || !result) { + logger.debug(`queryOneUserMappingByGithubUserId. Error. ${err}`); + return reject(err); + } + return resolve(result); + }); + }); +} + +/** + * Get single data by query parameters + * @param {Object} model The dynamoose model to query + * @param {Number} userId The The user id + * @returns {Promise} + */ +async function queryOneUserMappingByGitlabUserId(model, userId) { + return await new Promise((resolve, reject) => { + model.queryOne('gitlabUserId').eq(userId) + .all() + .exec((err, result) => { + if (err || !result) { + logger.debug(`queryOneUserMappingByGitlabUserId. Error. ${err}`); + return reject(err); + } + return resolve(result); + }); + }); +} + /** * Update collection * @param {Object} model The dynamoose model to update @@ -154,10 +280,10 @@ async function update(Model, id, data) { /** * Delete item in database * @param {Object} Model The dynamoose model to delete - * @param {Object} queryParams The query parameters object + * @param {String} id The id of copilot payment */ -async function remove(Model, queryParams) { - const dbItem = await scanOne(Model, queryParams); +async function removeCopilotPayment(Model, id) { + const dbItem = await getById(Model, id); await new Promise((resolve, reject) => { if (dbItem != null) { dbItem.delete((err) => { @@ -196,11 +322,17 @@ async function removeIssue(Model, repositoryId, number, provider) { module.exports = { getById, scan, - scanOne, updateMany, create, update, - remove, + queryOneActiveProject, queryOneIssue, + queryOneUserByType, + queryOneUserMappingByGithubUserId, + queryOneUserMappingByGitlabUserId, + queryOneUserMappingByGithubUsername, + queryOneUserMappingByGitlabUsername, + queryOneUserMappingByTCUsername, + removeCopilotPayment, removeIssue }; diff --git a/utils/topcoder-api-helper.js b/utils/topcoder-api-helper.js index 040476c..94d18b1 100644 --- a/utils/topcoder-api-helper.js +++ b/utils/topcoder-api-helper.js @@ -76,10 +76,9 @@ async function createChallenge(challenge) { timelineTemplateId: config.DEFAULT_TIMELINE_TEMPLATE_ID, projectId: challenge.projectId, trackId: config.DEFAULT_TRACK_ID, - // legacy:{ - // pureV5Task: true - // }, - tags:['Other'], + legacy: { + pureV5Task: true + }, startDate: new Date() }); try {