diff --git a/.circleci/config.yml b/.circleci/config.yml index 3a78bbe..9c4f07a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,11 +1,13 @@ version: 2 defaults: &defaults docker: - - image: circleci/python:2.7-stretch-browsers + - image: cimg/python:3.11.0-browsers install_dependency: &install_dependency name: Installation of build and deployment dependencies. command: | + sudo apt update sudo apt install jq + sudo apt install python3-pip sudo pip install awscli --upgrade sudo pip install docker-compose install_deploysuite: &install_deploysuite diff --git a/src/constants.js b/src/constants.js index 2e13372..497bf35 100644 --- a/src/constants.js +++ b/src/constants.js @@ -48,6 +48,23 @@ const challengeStatuses = { CancelledPaymentFailed: 'Cancelled - Payment Failed' } +const scorecardQuestionMapping = { + 30002212: [ + { + questionId: 30007531, + description: 'Does the submission sufficiently satisfy the requirements as described in the provided specification?' + }, + { + questionId: 30007533, + description: 'How would you rate the work ethic of this submitter?' + }, + { + questionId: 30007532, + description: 'How would you rate the quality of this submitters work?' + } + ] +} + const PhaseStatusTypes = { Scheduled: 1, Open: 2, @@ -155,5 +172,6 @@ module.exports = { challengeStatuses, PhaseStatusTypes, prizeTypesIds, - supportedMetadata + supportedMetadata, + scorecardQuestionMapping } diff --git a/src/services/ProcessorService.js b/src/services/ProcessorService.js index 19d2ddf..04a51b1 100644 --- a/src/services/ProcessorService.js +++ b/src/services/ProcessorService.js @@ -19,6 +19,7 @@ const paymentService = require('./paymentService') const { createOrSetNumberOfReviewers } = require('./selfServiceReviewerService') const { disableTimelineNotifications } = require('./selfServiceNotificationService') const legacyChallengeService = require('./legacyChallengeService') +const legacyChallengeReviewService = require('./legacyChallengeReviewService') /** * Drop and recreate phases in ifx @@ -51,6 +52,30 @@ async function recreatePhases (legacyId, v5Phases, createdBy) { phase.duration * 1000, createdBy ) + //Handle checkpoint phases + //Magic numbers: 15=checkpoint submission, 16=checkpoint screen, 17=checkpoint review, 1=registration + //For dependencyStart: 1=start, 0=end + if(phaseLegacyId==17){ + logger.info(`Creating phase dependencies for checkpoint phases`) + + const registrationPhaseId = await timelineService.getProjectPhaseId(legacyId, 1) + const checkpointSubmissionPhaseId = await timelineService.getProjectPhaseId(legacyId, 15) + const checkpointScreeningPhaseId = await timelineService.getProjectPhaseId(legacyId, 16) + const checkpointReviewPhaseId = await timelineService.getProjectPhaseId(legacyId, 17) + + await timelineService.insertPhaseDependency(registrationPhaseId, checkpointSubmissionPhaseId, 1, createdBy) + await timelineService.insertPhaseDependency(checkpointSubmissionPhaseId, checkpointScreeningPhaseId, 0, createdBy) + await timelineService.insertPhaseDependency(checkpointScreeningPhaseId, checkpointReviewPhaseId, 0, createdBy) + + logger.info(`Creating default scorecard records for checkpoint phases`) + //30001364 is the default checkpoint screening scorecard for studio (https://software.topcoder-dev.com/review/actions/ViewScorecard?scid=30001364) + await timelineService.insertScorecardId(checkpointScreeningPhaseId, 30001364, createdBy) + + //30001364 is the default checkpoint review scorecard for studio (https://software.topcoder-dev.com/review/actions/ViewScorecard?scid=30001004) + await timelineService.insertScorecardId(checkpointReviewPhaseId, 30001004, createdBy) + + + } } else if (!phaseLegacyId) { logger.warn(`Could not create phase ${phase.name} on legacy!`) } @@ -79,42 +104,53 @@ async function syncChallengePhases (legacyId, v5Phases, createdBy, isSelfService const phasesFromIFx = await timelineService.getChallengePhases(legacyId) logger.debug(`Phases from v5: ${JSON.stringify(v5Phases)}`) logger.debug(`Phases from IFX: ${JSON.stringify(phasesFromIFx)}`) - for (const phase of phasesFromIFx) { - const phaseName = _.get(_.find(phaseTypes, pt => pt.phase_type_id === phase.phase_type_id), 'name') - const v5Equivalent = _.find(v5Phases, p => p.name === phaseName) - logger.info(`v4 Phase: ${JSON.stringify(phase)}, v5 Equiv: ${JSON.stringify(v5Equivalent)}`) - if (v5Equivalent) { - // Compare duration and status - // if (v5Equivalent.duration * 1000 !== phase.duration * 1 || isSelfService) { - // || - // (v5Equivalent.isOpen && _.toInteger(phase.phase_status_id) === constants.PhaseStatusTypes.Closed) || - // (!v5Equivalent.isOpen && _.toInteger(phase.phase_status_id) === constants.PhaseStatusTypes.Open)) { - // const newStatus = v5Equivalent.isOpen - // ? constants.PhaseStatusTypes.Open - // : (new Date().getTime() <= new Date(v5Equivalent.scheduledEndDate).getTime() ? constants.PhaseStatusTypes.Scheduled : constants.PhaseStatusTypes.Closed) - // update phase - logger.debug(`Will update phase ${phaseName}/${v5Equivalent.name} from ${phase.duration} to duration ${v5Equivalent.duration * 1000} milli`) - const newStatus = v5Equivalent.isOpen - ? constants.PhaseStatusTypes.Open - : (_.toInteger(phase.phase_status_id) === constants.PhaseStatusTypes.Scheduled ? constants.PhaseStatusTypes.Scheduled : constants.PhaseStatusTypes.Closed) - await timelineService.updatePhase( - phase.project_phase_id, - legacyId, - v5Equivalent.scheduledStartDate, - v5Equivalent.scheduledEndDate, - v5Equivalent.duration * 1000, - newStatus // phase.phase_status_id - ) - // newStatus) - // } else { - // logger.info(`Durations for ${phaseName} match: ${v5Equivalent.duration * 1000} === ${phase.duration}`) - // } - } else { - logger.info(`No v5 Equivalent Found for ${phaseName}`) + let phaseGroups = {} + _.forEach(phasesFromIFx, p => { + if (!phaseGroups[p.phase_type_id]) { + phaseGroups[p.phase_type_id] = [] } - if (isSelfService && phaseName === 'Review') { - // make sure to set the required reviewers to 2 - await createOrSetNumberOfReviewers(_.toString(phase.project_phase_id), _.toString(numOfReviewers), _.toString(createdBy)) + phaseGroups[p.phase_type_id].push(p) + }) + _.forEach(_.cloneDeep(phaseGroups), (pg, pt) => { + phaseGroups[pt] = _.sortBy(pg, 'scheduled_start_time') + }) + + for (const key of _.keys(phaseGroups)) { + let phaseOrder = 0 + let v5Equivalents = undefined + for (const phase of phaseGroups[key]) { + const phaseName = _.get(_.find(phaseTypes, pt => pt.phase_type_id === phase.phase_type_id), 'name') + if (_.isUndefined(v5Equivalents)) { + v5Equivalents = _.sortBy(_.filter(v5Phases, p => p.name === phaseName), 'scheduledStartDate') + } + if (v5Equivalents.length > 0) { + if (v5Equivalents.length === phaseGroups[key].length) { + const v5Equivalent = v5Equivalents[phaseOrder] + logger.debug(`Will update phase ${phaseName}/${v5Equivalent.name} from ${phase.duration} to duration ${v5Equivalent.duration * 1000} milli`) + let newStatus = _.toInteger(phase.phase_status_id) + if (v5Equivalent.isOpen && _.toInteger(phase.phase_status_id) === constants.PhaseStatusTypes.Closed) { + newStatus = constants.PhaseStatusTypes.Scheduled + } + await timelineService.updatePhase( + phase.project_phase_id, + legacyId, + phase.fixed_start_time ? v5Equivalent.scheduledStartDate : null, + v5Equivalent.scheduledStartDate, + v5Equivalent.scheduledEndDate, + v5Equivalent.duration * 1000, + newStatus + ) + } else { + logger.info(`number of ${phaseName} does not match`) + } + } else { + logger.info(`No v5 Equivalent Found for ${phaseName}`) + } + if (isSelfService && phaseName === 'Review') { + // make sure to set the required reviewers to 2 + await createOrSetNumberOfReviewers(_.toString(phase.project_phase_id), _.toString(numOfReviewers), _.toString(createdBy)) + } + phaseOrder = phaseOrder + 1 } } // TODO: What about iterative reviews? There can be many for the same challenge. @@ -701,6 +737,28 @@ async function processMessage (message) { throw new Error(`Error getting challenge by id - Error: ${JSON.stringify(e)}`) } + // If iterative review is open + if (_.find(_.get(message.payload, 'phases'), p => p.isOpen && p.name === 'Iterative Review')) { + // Try to read reviews and insert them into informix DB + if (message.payload.metadata && message.payload.legacy.reviewScorecardId) { + let orReviewFeedback = _.find(message.payload.metadata, meta => meta.name === 'or_review_feedback') + let orReviewScore = _.find(message.payload.metadata, meta => meta.name === 'or_review_score') + if (!_.isUndefined(orReviewFeedback) && !_.isUndefined(orReviewScore)) { + orReviewFeedback = JSON.parse(orReviewFeedback) + const reviewResponses = [] + _.each(orReviewFeedback, (value, key) => { + const questionId = _.get(_.find(constants.scorecardQuestionMapping[message.payload.legacy.reviewScorecardId], item => _.toString(item.questionId) === _.toString(key) || _.toLower(item.description) === _.toLower(key)), 'questionId') + reviewResponses.push({ + questionId, + answer: value + }) + }) + orReviewScore = _.toNumber(orReviewFeedback) + await legacyChallengeReviewService.insertReview(legacyId, message.payload.legacy.reviewScorecardId, orReviewScore, reviewResponses, createdByUserId) + } + } + } + if (message.payload.status && challenge) { // Whether we need to sync v4 ES again let needSyncV4ES = false @@ -788,7 +846,7 @@ processMessage.schema = { prizeSets: Joi.array().items(Joi.object().keys({ type: Joi.string().valid(_.values(constants.prizeSetTypes)).required(), prizes: Joi.array().items(Joi.object().keys({ - value: Joi.number().positive().required() + value: Joi.number().min(0).required() }).unknown(true)) }).unknown(true)).min(1), tags: Joi.array().items(Joi.string().required()).min(1), // tag names diff --git a/src/services/legacyChallengeReviewService.js b/src/services/legacyChallengeReviewService.js new file mode 100644 index 0000000..f003f0a --- /dev/null +++ b/src/services/legacyChallengeReviewService.js @@ -0,0 +1,124 @@ +/** + * Legacy Challenge Service + * Interacts with InformixDB + * Note: this is built to work for topgear challenges and iterative review phases + */ +const _ = require('lodash') +const logger = require('../common/logger') +const util = require('util') +const helper = require('../common/helper') +const IDGenerator = require('../common/idGenerator') +const reviewIdGen = new IDGenerator('review_id_seq') +const reviewItemIdGen = new IDGenerator('review_item_id_seq') + +const ITERATIVE_REVIEWER_RESOURCE_ROLE_ID = 21 +const QUERY_GET_ITERATIVE_REVIEW_RESOURCE_FOR_CHALLENGE = `SELECT limit 1 resource_id as resourceid FROM resource WHERE project_id = %d AND resource_role_id = ${ITERATIVE_REVIEWER_RESOURCE_ROLE_ID}` + +const QUERY_CREATE_REVIEW = 'INSERT INTO review (review_id, resource_id, submission_id, project_phase_id, scorecard_id, committed, score, initial_score, create_user, create_date, modify_user, modify_date) values (?,?,?,?,?,?,?,?,?,CURRENT,?,CURRENT)' + +const QUERY_CREATE_REVIEW_ITEM = 'INSERT INTO review_item (review_item_id, review_id, scorecard_question_id, upload_id, answer, sort, create_user, create_date, modify_user, modify_date) values (?,?,?,?,?,?,?,CURRENT,?,CURRENT)' + +const QUERY_GET_SUBMISSION = 'SELECT FIRST 1 * FROM submission s INNER JOIN upload u on s.upload_id = u.upload_id WHERE u.project_id = %d AND upload_status_id = 1 AND submission_status_id = 1 ORDER BY u.CREATE_DATE ASC' + +const QUERY_GET_PROJECT_PHASE = 'select pc.parameter scorecard_id, pp.project_phase_id project_phase_id from project_phase pp inner join phase_criteria pc on pc.project_phase_id = pp.project_phase_id where pp.project_id = %d and pp.phase_type_id = 18 and phase_criteria_type_id = 1' + +/** + * Prepare Informix statement + * @param {Object} connection the Informix connection + * @param {String} sql the sql + * @return {Object} Informix statement + */ +async function prepare (connection, sql) { + // logger.debug(`Preparing SQL ${sql}`) + const stmt = await connection.prepareAsync(sql) + return Promise.promisifyAll(stmt) +} + +/** + * Insert review in IFX + * @param {Number} challengeLegacyId the legacy challenge ID + * @param {Number} createdBy the scorecard ID + * @param {Number} score the review score + * @param {Array} responses the review responses + * @param {Number} createdBy the creator user ID + */ +async function insertReview (challengeLegacyId, scorecardId, score, responses, createdBy) { + const connection = await helper.getInformixConnection() + let result = null + let reviewId + try { + const resourceId = await getIterativeReviewerResourceId(connection, challengeLegacyId) + if (!resourceId) throw new Error('Cannot find Iterative Reviewer') + const submissionId = await getSubmissionId(connection, challengeLegacyId) + if (!submissionId) throw new Error('Cannot find Submission') + const projectPhaseId = await getProjectPhaseId(connection, challengeLegacyId) + if (!projectPhaseId) throw new Error('Cannot find Project Phase Id') + reviewId = await reviewIdGen.getNextId() + await connection.beginTransactionAsync() + const query = await prepare(connection, QUERY_CREATE_REVIEW) + result = await query.executeAsync([reviewId, resourceId, submissionId, projectPhaseId, scorecardId, 1, score, score, createdBy, createdBy]) + for (let i = 0; i < responses.length; i += 1) { + await insertReviewItem(connection, reviewId, responses[i], i, createdBy) + } + await connection.commitTransactionAsync() + } catch (e) { + logger.error(`Error in 'insertReview' ${e}, rolling back transaction`) + await connection.rollbackTransactionAsync() + throw e + } finally { + logger.info(`Review ${challengeLegacyId} has been created`) + await connection.closeAsync() + } + return result +} + +/** + * Insert review item in IFX + * @param {Object} connection + * @param {Number} reviewId the review ID + * @param {Object} response the response + * @param {Number} sort the sort + * @param {Number} createdBy the creator user ID + */ +async function insertReviewItem (connection, reviewId, response, sort, createdBy) { + let result = null + const reviewItemId = await reviewItemIdGen.getNextId() + await connection.beginTransactionAsync() + const query = await prepare(connection, QUERY_CREATE_REVIEW_ITEM) + result = await query.executeAsync([reviewItemId, reviewId, response.questionId, null, response.answer, sort, createdBy, createdBy]) + return result +} + +/** + * Gets the iterative reviewer resource id + * @param {Object} connection + * @param {Number} challengeLegacyId + */ +async function getIterativeReviewerResourceId (connection, challengeLegacyId) { + const result = await connection.queryAsync(util.format(QUERY_GET_ITERATIVE_REVIEW_RESOURCE_FOR_CHALLENGE, challengeLegacyId)) + return _.get(result, '[0].resourceid', null) +} + +/** + * Gets the submission id + * @param {Object} connection + * @param {Number} challengeLegacyId + */ +async function getSubmissionId (connection, challengeLegacyId) { + const result = await connection.queryAsync(util.format(QUERY_GET_SUBMISSION, challengeLegacyId)) + return _.get(result, '[0].submission_id', null) +} + +/** + * Gets the submission id + * @param {Object} connection + * @param {Number} challengeLegacyId + */ +async function getProjectPhaseId (connection, challengeLegacyId) { + const result = await connection.queryAsync(util.format(QUERY_GET_PROJECT_PHASE, challengeLegacyId)) + return _.get(result, '[0].project_phase_id', null) +} + +module.exports = { + insertReview +} diff --git a/src/services/timelineService.js b/src/services/timelineService.js index 81a2d0b..fb8a2bf 100644 --- a/src/services/timelineService.js +++ b/src/services/timelineService.js @@ -2,6 +2,8 @@ * Timeline Service * Interacts with InformixDB */ +const _ = require('lodash') + const logger = require('../common/logger') const util = require('util') const config = require('config') @@ -9,14 +11,14 @@ const momentTZ = require('moment-timezone') const IDGenerator = require('../common/idGenerator') const helper = require('../common/helper') -const phaseIdGen = new IDGenerator('prize_id_seq') +const phaseIdGen = new IDGenerator('project_phase_id_seq') const QUERY_GET_PHASE_TYPES = 'SELECT phase_type_id, name FROM phase_type_lu' -const QUERY_GET_CHALLENGE_PHASES = 'SELECT project_phase_id, scheduled_start_time, scheduled_end_time, duration, phase_status_id, phase_type_id FROM project_phase WHERE project_id = %d' +const QUERY_GET_CHALLENGE_PHASES = 'SELECT project_phase_id, fixed_start_time, scheduled_start_time, scheduled_end_time, duration, phase_status_id, phase_type_id FROM project_phase WHERE project_id = %d' const QUERY_DROP_CHALLENGE_PHASE = 'DELETE FROM project_phase WHERE project_id = ? AND project_phase_id = ?' -const QUERY_INSERT_CHALLENGE_PHASE = 'INSERT INTO project_phase (project_phase_id, project_id, phase_type_id, phase_status_id, fixed_start_time, scheduled_start_time, scheduled_end_time, actual_start_time, actual_end_time, duration, create_user, create_date, modify_user, modify_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT, ?, CURRENT)' -const QUERY_UPDATE_CHALLENGE_PHASE = 'UPDATE project_phase SET scheduled_start_time = ?, scheduled_end_time = ?, duration = ?, phase_status_id = ? WHERE project_phase_id = %d and project_id = %d' +const QUERY_INSERT_CHALLENGE_PHASE = 'INSERT INTO project_phase (project_phase_id, project_id, phase_type_id, phase_status_id, scheduled_start_time, scheduled_end_time, duration, create_user, create_date, modify_user, modify_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT, ?, CURRENT)' +const QUERY_UPDATE_CHALLENGE_PHASE = 'UPDATE project_phase SET fixed_start_time = ?, scheduled_start_time = ?, scheduled_end_time = ?, duration = ?, phase_status_id = ? WHERE project_phase_id = %d and project_id = %d' const QUERY_DROP_CHALLENGE_PHASE_CRITERIA = 'DELETE FROM phase_criteria WHERE project_phase_id = ?' @@ -25,11 +27,19 @@ const QUERY_GET_TIMELINE_NOTIFICATION_SETTINGS = 'SELECT value FROM project_info const QUERY_CREATE_TIMELINE_NOTIFICATIONS = 'INSERT INTO project_info (project_id, project_info_type_id, value, create_user, create_date, modify_user, modify_date) VALUES (?, "11", "On", ?, CURRENT, ?, CURRENT)' const QUERY_UPDATE_TIMELINE_NOTIFICATIONS = 'UPDATE project_info SET value = "On", modify_user = ?, modify_date = CURRENT WHERE project_info_type_id = "11" AND project_id = ?' +const QUERY_INSERT_CHALLENGE_PHASE_DEPENDENCY = 'INSERT INTO phase_dependency (dependency_phase_id, dependent_phase_id, dependency_start, dependent_start, lag_time, create_user, create_date, modify_user, modify_date) VALUES (?, ?, ?, 1, 0, ?, CURRENT, ?, CURRENT)' +const QUERY_GET_PROJECT_PHASE_ID = 'SELECT project_phase_id as project_phase_id FROM project_phase WHERE project_id = %d AND phase_type_id = %d' + +const QUERY_INSERT_CHALLENGE_PHASE_SCORECARD_ID = 'INSERT INTO phase_criteria (project_phase_id, phase_criteria_type_id, parameter, create_user, create_date, modify_user, modify_date) VALUES (?, 1, ?, ?, CURRENT, ?, CURRENT)' + /** * Formats a date into a format supported by ifx * @param {String} dateStr the date in string format */ function formatDate (dateStr) { + if (!dateStr) { + return null + } const date = momentTZ.tz(dateStr, config.TIMEZONE).format('YYYY-MM-DD HH:mm:ss') logger.info(`Formatting date ${dateStr} New Date ${date}`) return date @@ -64,6 +74,61 @@ async function getPhaseTypes () { return result } +async function insertPhaseDependency(dependencyPhaseId, dependentPhaseId, dependencyStart, createdBy){ + + logger.info(`Creating phase dependency ${dependencyPhaseId} to ${dependentPhaseId} at ${dependencyStart}`) + const connection = await helper.getInformixConnection() + let result = null + try { + let query = await prepare(connection, QUERY_INSERT_CHALLENGE_PHASE_DEPENDENCY) + result = await query.executeAsync([dependencyPhaseId, dependentPhaseId, dependencyStart, createdBy, createdBy]) + } catch (e) { + logger.error(`Error in 'insertPhaseDependency' ${e}`) + throw e + } finally { + await connection.closeAsync() + } + return result +} + + +async function insertScorecardId(projectPhaseId, scorecardId, createdBy){ + + logger.info(`Creating scorecard ID ${projectPhaseId} use scorecard ${scorecardId}`) + const connection = await helper.getInformixConnection() + let result = null + try { + let query = await prepare(connection, QUERY_INSERT_CHALLENGE_PHASE_SCORECARD_ID) + result = await query.executeAsync([projectPhaseId, scorecardId, createdBy, createdBy]) + } catch (e) { + logger.error(`Error in 'insertScorecardId' ${e}`) + throw e + } finally { + await connection.closeAsync() + } + return result +} + +/** + * Gets phase for the given phase type for the given challenge ID + */ +async function getProjectPhaseId(challengeLegacyId, phaseTypeId) { + logger.info(`Getting project phase ID type ${phaseTypeId} for challenge ${challengeLegacyId}`) + const connection = await helper.getInformixConnection() + let result = null + try { + await connection.beginTransactionAsync() + result = await connection.queryAsync(util.format(QUERY_GET_PROJECT_PHASE_ID, challengeLegacyId, phaseTypeId)) + } catch (e) { + logger.error(`Error in 'getProjectPhaseId' ${e}`) + throw e + } finally { + await connection.closeAsync() + } + const project_phase_id = _.get(result, '[0].project_phase_id', null) + logger.info(`Project phase ID: ${project_phase_id}`) + return project_phase_id +} /** * Drop challenge phase * @param {Number} challengeLegacyId the legacy challenge ID @@ -133,10 +198,7 @@ async function createPhase (challengeLegacyId, phaseTypeId, statusTypeId, schedu phaseTypeId, statusTypeId, formatDate(scheduledStartDate), - formatDate(scheduledStartDate), formatDate(scheduledEndDate), - formatDate(actualStartDate), - formatDate(actualEndDate), duration, createdBy, createdBy @@ -147,15 +209,13 @@ async function createPhase (challengeLegacyId, phaseTypeId, statusTypeId, schedu phaseTypeId, statusTypeId, formatDate(scheduledStartDate), - formatDate(scheduledStartDate), formatDate(scheduledEndDate), - formatDate(actualStartDate), - formatDate(actualEndDate), duration, createdBy, createdBy ]) await connection.commitTransactionAsync() + } catch (e) { logger.error(`Error in 'createPhase' ${e}, rolling back transaction`) await connection.rollbackTransactionAsync() @@ -171,18 +231,19 @@ async function createPhase (challengeLegacyId, phaseTypeId, statusTypeId, schedu * Update a phase in IFX * @param {Number} phaseId the phase ID * @param {Number} challengeLegacyId the legacy challenge ID + * @param {Date} fixedStartTime the fixed start date * @param {Date} startTime the scheduled start date * @param {Date} endTime the scheduled end date * @param {Date} duration the duration * @param {Number} statusTypeId the status type ID */ -async function updatePhase (phaseId, challengeLegacyId, startTime, endTime, duration, statusTypeId) { +async function updatePhase (phaseId, challengeLegacyId, fixedStartTime, startTime, endTime, duration, statusTypeId) { const connection = await helper.getInformixConnection() let result = null try { // await connection.beginTransactionAsync() const query = await prepare(connection, util.format(QUERY_UPDATE_CHALLENGE_PHASE, phaseId, challengeLegacyId)) - result = await query.executeAsync([formatDate(startTime), formatDate(endTime), duration, statusTypeId]) + result = await query.executeAsync([formatDate(fixedStartTime), formatDate(startTime), formatDate(endTime), duration, statusTypeId]) // await connection.commitTransactionAsync() } catch (e) { logger.error(`Error in 'updatePhase' ${e}, rolling back transaction`) @@ -250,5 +311,8 @@ module.exports = { updatePhase, enableTimelineNotifications, createPhase, - dropPhase + dropPhase, + insertPhaseDependency, + getProjectPhaseId, + insertScorecardId }