diff --git a/.circleci/config.yml b/.circleci/config.yml index 9c4f07a..ae80cb8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -72,6 +72,7 @@ workflows: branches: only: - develop + - PLAT-2032 # Production builds are exectuted only on tagged commits to the # master branch. diff --git a/compose-dev.yaml b/compose-dev.yaml new file mode 100644 index 0000000..de3d08b --- /dev/null +++ b/compose-dev.yaml @@ -0,0 +1,13 @@ +services: + app: + entrypoint: + - sleep + - infinity + image: node:10.15.1 + platform: linux/amd64 + init: true + volumes: + - type: bind + source: /var/run/docker.sock + target: /var/run/docker.sock + diff --git a/package-lock.json b/package-lock.json index 0f4ba98..1953e3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7836,4 +7836,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index d36619d..bdfd14b 100644 --- a/package.json +++ b/package.json @@ -58,5 +58,8 @@ "test/unit/test.js", "test/e2e/test.js" ] + }, + "volta": { + "node": "12.22.12" } } diff --git a/src/common/helper.js b/src/common/helper.js index 281a377..ec10824 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -4,6 +4,7 @@ const _ = require('lodash') const config = require('config') +const momentTZ = require('moment-timezone') const ifxnjs = require('ifxnjs') const request = require('superagent') const m2mAuth = require('tc-core-library-js').auth.m2m @@ -190,6 +191,19 @@ async function getMemberIdByHandle (handle) { return memberId } +/** + * 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 +} + module.exports = { getInformixConnection, getKafkaOptions, @@ -200,5 +214,6 @@ module.exports = { postRequest, postBusEvent, forceV4ESFeeder, - getMemberIdByHandle + getMemberIdByHandle, + formatDate } diff --git a/src/services/ProcessorService.js b/src/services/ProcessorService.js index 04a51b1..992135b 100644 --- a/src/services/ProcessorService.js +++ b/src/services/ProcessorService.js @@ -20,6 +20,7 @@ const { createOrSetNumberOfReviewers } = require('./selfServiceReviewerService') const { disableTimelineNotifications } = require('./selfServiceNotificationService') const legacyChallengeService = require('./legacyChallengeService') const legacyChallengeReviewService = require('./legacyChallengeReviewService') +const phaseCriteriaService = require('./phaseCriteriaService'); /** * Drop and recreate phases in ifx @@ -92,6 +93,79 @@ async function recreatePhases (legacyId, v5Phases, createdBy) { logger.info('recreatePhases :: end') } +async function addPhaseConstraints(legacyId, v5Phases, createdBy) { + logger.info(`addPhaseConstraints :: start: ${legacyId}, ${JSON.stringify(v5Phases)}`) + const allPhaseCriteria = await phaseCriteriaService.getPhaseCriteria(); + logger.info(`addPhaseConstraints :: allPhaseCriteria: ${JSON.stringify(allPhaseCriteria)}`) + + const phaseTypes = await timelineService.getPhaseTypes() + logger.info(`addPhaseConstraints :: phaseTypes: ${JSON.stringify(phaseTypes)}`) + + const phasesFromIFx = await timelineService.getChallengePhases(legacyId) + + for (const phase of v5Phases) { + logger.info(`addPhaseConstraints :: phase: ${legacyId} -> ${JSON.stringify(phase)}`) + if (phase.constraints == null || phase.constraints.length === 0) continue; + + const phaseLegacyId = _.get(_.find(phaseTypes, pt => pt.name === phase.name), 'phase_type_id') + const existingLegacyPhase = _.find(phasesFromIFx, p => p.phase_type_id === phaseLegacyId) + + const projectPhaseId = _.get(existingLegacyPhase, 'project_phase_id') + if (!projectPhaseId) { + logger.warn(`Could not find phase ${phase.name} on legacy!`) + continue + } + + let constraintName = null; + let constraintValue = null + + if (phase.name === 'Submission') { + const numSubmissionsConstraint = phase.constraints.find(c => c.name === 'Number of Submissions') + if (numSubmissionsConstraint) { + constraintName = 'Submission Number' + constraintValue = numSubmissionsConstraint.value + } + } + + if (phase.name === 'Registration') { + const numRegistrantsConstraint = phase.constraints.find(c => c.name === 'Number of Registrants') + if (numRegistrantsConstraint) { + constraintName = 'Registration Number' + constraintValue = numRegistrantsConstraint.value + } + } + + if (phase.name === 'Review') { + const numReviewersConstraint = phase.constraints.find(c => c.name === 'Number of Reviewers') + if (numReviewersConstraint) { + constraintName = 'Reviewer Number' + constraintValue = numReviewersConstraint.value + } + } + + // We have an interesting situation if a submission phase constraint was added but + // no registgration phase constraint was added. This ends up opening Post-Mortem + // phase if registration closes with 0 submissions. + // For now I'll leave it as is and handle this better in the new Autopilot implementation + // A quick solution would have been adding a registration constraint with value 1 if none is provided when there is a submission phase constraint + + if (constraintName && constraintValue) { + const phaseCriteriaTypeId = _.get(_.find(allPhaseCriteria, pc => pc.name === constraintName), 'phase_criteria_type_id') + if (phaseCriteriaTypeId) { + logger.debug(`Will create phase constraint ${constraintName} with value ${constraintValue}`) + // Ideally we should update the existing phase criteria, but this processor will go away in weeks + // and it's a backend processor, so we can just drop and recreate without slowing down anything + await phaseCriteriaService.dropPhaseCriteria(projectPhaseId, phaseCriteriaTypeId) + await phaseCriteriaService.createPhaseCriteria(projectPhaseId, phaseCriteriaTypeId, constraintValue, createdBy) + } else { + logger.warn(`Could not find phase criteria type for ${constraintName}`) + } + } + + } + logger.info('addPhaseConstraints :: end') +} + /** * Sync the information from the v5 phases into legacy * @param {Number} legacyId the legacy challenge ID @@ -99,7 +173,7 @@ async function recreatePhases (legacyId, v5Phases, createdBy) { * @param {Boolean} isSelfService is the challenge self-service * @param {String} createdBy the created by */ -async function syncChallengePhases (legacyId, v5Phases, createdBy, isSelfService, numOfReviewers) { +async function syncChallengePhases (legacyId, v5Phases, createdBy, isSelfService, numOfReviewers, isBeingActivated) { const phaseTypes = await timelineService.getPhaseTypes() const phasesFromIFx = await timelineService.getChallengePhases(legacyId) logger.debug(`Phases from v5: ${JSON.stringify(v5Phases)}`) @@ -131,6 +205,17 @@ async function syncChallengePhases (legacyId, v5Phases, createdBy, isSelfService if (v5Equivalent.isOpen && _.toInteger(phase.phase_status_id) === constants.PhaseStatusTypes.Closed) { newStatus = constants.PhaseStatusTypes.Scheduled } + + if (isBeingActivated && ['Registration', 'Submission'].indexOf(v5Equivalent.name) != -1) { + const scheduledStartDate = v5Equivalent.scheduledStartDate; + const now = new Date().getTime(); + if (scheduledStartDate != null && new Date(scheduledStartDate).getTime() < now) { + newStatus = constants.PhaseStatusTypes.Open; + } + + logger.debug(`Challenge phase ${v5Equivalent.name} status is being set to: ${newStatus} on challenge activation.`) + } + await timelineService.updatePhase( phase.project_phase_id, legacyId, @@ -138,7 +223,8 @@ async function syncChallengePhases (legacyId, v5Phases, createdBy, isSelfService v5Equivalent.scheduledStartDate, v5Equivalent.scheduledEndDate, v5Equivalent.duration * 1000, - newStatus + newStatus, + isBeingActivated && newStatus == constants.PhaseStatusTypes.Open ? new Date() : null ) } else { logger.info(`number of ${phaseName} does not match`) @@ -674,6 +760,7 @@ async function processMessage (message) { const createdByUserHandle = _.get(message, 'payload.createdBy') const updatedByUserHandle = _.get(message, 'payload.updatedBy') + const updatedAt = _.get(message, 'payload.updated', new Date().toISOString()) const createdByUserId = await helper.getMemberIdByHandle(createdByUserHandle) let updatedByUserId = createdByUserId @@ -759,11 +846,14 @@ async function processMessage (message) { } } + let isBeingActivated = false; + if (message.payload.status && challenge) { // Whether we need to sync v4 ES again let needSyncV4ES = false // logger.info(`The status has changed from ${challenge.currentStatus} to ${message.payload.status}`) if (message.payload.status === constants.challengeStatuses.Active && challenge.currentStatus !== constants.challengeStatuses.Active) { + isBeingActivated = true; logger.info('Activating challenge...') const activated = await activateChallenge(legacyId) logger.info(`Activated! ${JSON.stringify(activated)}`) @@ -790,16 +880,20 @@ async function processMessage (message) { if (!_.get(message.payload, 'task.isTask')) { const numOfReviewers = 2 - await syncChallengePhases(legacyId, message.payload.phases, createdByUserId, _.get(message, 'payload.legacy.selfService'), numOfReviewers) + await syncChallengePhases(legacyId, message.payload.phases, createdByUserId, _.get(message, 'payload.legacy.selfService'), numOfReviewers, isBeingActivated) + await addPhaseConstraints(legacyId, message.payload.phases, createdByUserId); needSyncV4ES = true } else { logger.info('Will skip syncing phases as the challenge is a task...') } if (message.payload.status === constants.challengeStatuses.CancelledClientRequest && challenge.currentStatus !== constants.challengeStatuses.CancelledClientRequest) { logger.info('Cancelling challenge...') - await legacyChallengeService.cancelChallenge(legacyId, updatedByUserId) + await legacyChallengeService.cancelChallenge(legacyId, updatedByUserId, updatedAt) needSyncV4ES = true + } else { + await legacyChallengeService.updateChallengeAudit(legacyId, updatedByUserId, updatedAt) } + if (needSyncV4ES) { try { logger.info(`Resync V4 ES for the legacy challenge ${legacyId}`) diff --git a/src/services/legacyChallengeService.js b/src/services/legacyChallengeService.js index 97eb0a5..e95b981 100644 --- a/src/services/legacyChallengeService.js +++ b/src/services/legacyChallengeService.js @@ -7,7 +7,8 @@ const util = require('util') const helper = require('../common/helper') const { createChallengeStatusesMap } = require('../constants') -const QUERY_UPDATE_PROJECT = 'UPDATE project SET project_status_id = ?, modify_user = ? WHERE project_id = %d' +const QUERY_UPDATE_PROJECT = 'UPDATE project SET project_status_id = ?, modify_user = ?, modify_date = ? WHERE project_id = %d' +const QUERY_UPDATE_PROJECT_AUDIT = 'UPDATE project SET modify_user = ?, modify_date = ? WHERE project_id = %d' /** * Prepare Informix statement @@ -24,15 +25,16 @@ async function prepare (connection, sql) { /** * Update a challenge in IFX * @param {Number} challengeLegacyId the legacy challenge ID - * @param {Number} createdBy the creator user ID + * @param {Number} updatedBy the user ID + * @param {String} updatedAt the challenge modified time */ -async function cancelChallenge (challengeLegacyId, createdBy) { +async function cancelChallenge (challengeLegacyId, updatedBy, updatedAt) { const connection = await helper.getInformixConnection() let result = null try { await connection.beginTransactionAsync() const query = await prepare(connection, util.format(QUERY_UPDATE_PROJECT, challengeLegacyId)) - result = await query.executeAsync([createChallengeStatusesMap.CancelledClientRequest, createdBy]) + result = await query.executeAsync([createChallengeStatusesMap.CancelledClientRequest, updatedBy, helper.formatDate(updatedAt)]) await connection.commitTransactionAsync() } catch (e) { logger.error(`Error in 'cancelChallenge' ${e}, rolling back transaction`) @@ -45,6 +47,29 @@ async function cancelChallenge (challengeLegacyId, createdBy) { return result } +/** + * Update a challenge audit fields in IFX + * @param {Number} challengeLegacyId the legacy challenge ID + * @param {Number} updatedBy the user ID + * @param {String} updatedAt the challenge modified time + */ +async function updateChallengeAudit (challengeLegacyId, updatedBy, updatedAt) { + const connection = await helper.getInformixConnection() + let result = null + try { + const query = await prepare(connection, util.format(QUERY_UPDATE_PROJECT_AUDIT, challengeLegacyId)) + result = await query.executeAsync([updatedBy, helper.formatDate(updatedAt)]) + } catch (e) { + logger.error(`Error in 'updateChallengeAudit' ${e}`) + throw e + } finally { + logger.info(`Challenge audit for ${challengeLegacyId} has been updated`) + await connection.closeAsync() + } + return result +} + module.exports = { - cancelChallenge + cancelChallenge, + updateChallengeAudit } diff --git a/src/services/phaseCriteriaService.js b/src/services/phaseCriteriaService.js new file mode 100644 index 0000000..dd00e48 --- /dev/null +++ b/src/services/phaseCriteriaService.js @@ -0,0 +1,81 @@ +/** + * Number of reviewers Service + * Interacts with InformixDB + */ +const util = require('util') +const logger = require('../common/logger') +const helper = require('../common/helper') + +const QUERY_GET_PHASE_CRITERIA = 'SELECT phase_criteria_type_id, name FROM phase_criteria_type_lu;' +const QUERY_CREATE = 'INSERT INTO phase_criteria (project_phase_id, phase_criteria_type_id, parameter, create_user, create_date, modify_user, modify_date) VALUES (?, ?, ?, ?, CURRENT, ?, CURRENT)' +const QUERY_DELETE = 'DELETE FROM phase_criteria WHERE project_phase_id = ? AND phase_criteria_type_id = ?' + +/** + * 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) +} + +async function getPhaseCriteria () { + const connection = await helper.getInformixConnection() + let result = null + try { + result = await connection.queryAsync(QUERY_GET_PHASE_CRITERIA) + } catch (e) { + logger.error(`Error in 'getPhaseCriteria' ${e}`) + throw e + } finally { + await connection.closeAsync() + } + return result +} + +async function dropPhaseCriteria(phaseId, phaseCriteriaTypeId) { + const connection = await helper.getInformixConnection() + let result = null + try { + await connection.beginTransactionAsync() + const query = await prepare(connection, QUERY_DELETE) + result = await query.executeAsync([phaseId, phaseCriteriaTypeId]) + await connection.commitTransactionAsync() + } catch (e) { + logger.error(`Error in 'dropPhaseCriteria' ${e}`) + await connection.rollbackTransactionAsync() + throw e + } finally { + await connection.closeAsync() + } + return result +} + +async function createPhaseCriteria(phaseId, phaseCriteriaTypeId, value, createdBy) { + const connection = await helper.getInformixConnection() + let result = null + try { + await connection.beginTransactionAsync() + const query = await prepare(connection, QUERY_CREATE) + result = await query.executeAsync([phaseId, phaseCriteriaTypeId, value, createdBy, createdBy]) + await connection.commitTransactionAsync() + } catch (e) { + logger.error(`Error in 'createPhaseCriteria' ${e}`) + await connection.rollbackTransactionAsync() + throw e + } finally { + await connection.closeAsync() + } + return result +} + + + +module.exports = { + getPhaseCriteria, + createPhaseCriteria, + dropPhaseCriteria +} diff --git a/src/services/timelineService.js b/src/services/timelineService.js index fb8a2bf..6502dc1 100644 --- a/src/services/timelineService.js +++ b/src/services/timelineService.js @@ -7,7 +7,6 @@ const _ = require('lodash') const logger = require('../common/logger') const util = require('util') const config = require('config') -const momentTZ = require('moment-timezone') const IDGenerator = require('../common/idGenerator') const helper = require('../common/helper') @@ -19,6 +18,7 @@ const QUERY_GET_CHALLENGE_PHASES = 'SELECT project_phase_id, fixed_start_time, s 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, 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_UPDATE_CHALLENGE_PHASE_WITH_START_TIME = 'UPDATE project_phase SET fixed_start_time = ?, scheduled_start_time = ?, scheduled_end_time = ?, duration = ?, phase_status_id = ?, actual_start_time = ? WHERE project_phase_id = %d and project_id = %d' const QUERY_DROP_CHALLENGE_PHASE_CRITERIA = 'DELETE FROM phase_criteria WHERE project_phase_id = ?' @@ -32,19 +32,6 @@ const QUERY_GET_PROJECT_PHASE_ID = 'SELECT project_phase_id as project_phase_id 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 -} - /** * Prepare Informix statement * @param {Object} connection the Informix connection @@ -197,8 +184,8 @@ async function createPhase (challengeLegacyId, phaseTypeId, statusTypeId, schedu challengeLegacyId, phaseTypeId, statusTypeId, - formatDate(scheduledStartDate), - formatDate(scheduledEndDate), + helper.formatDate(scheduledStartDate), + helper.formatDate(scheduledEndDate), duration, createdBy, createdBy @@ -208,8 +195,8 @@ async function createPhase (challengeLegacyId, phaseTypeId, statusTypeId, schedu challengeLegacyId, phaseTypeId, statusTypeId, - formatDate(scheduledStartDate), - formatDate(scheduledEndDate), + helper.formatDate(scheduledStartDate), + helper.formatDate(scheduledEndDate), duration, createdBy, createdBy @@ -237,13 +224,19 @@ async function createPhase (challengeLegacyId, phaseTypeId, statusTypeId, schedu * @param {Date} duration the duration * @param {Number} statusTypeId the status type ID */ -async function updatePhase (phaseId, challengeLegacyId, fixedStartTime, startTime, endTime, duration, statusTypeId) { +async function updatePhase (phaseId, challengeLegacyId, fixedStartTime, startTime, endTime, duration, statusTypeId, actualStartTime) { 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(fixedStartTime), formatDate(startTime), formatDate(endTime), duration, statusTypeId]) + const query = actualStartTime == null ? + await prepare(connection, util.format(QUERY_UPDATE_CHALLENGE_PHASE, phaseId, challengeLegacyId)) : + await prepare(connection, util.format(QUERY_UPDATE_CHALLENGE_PHASE_WITH_START_TIME, phaseId, challengeLegacyId)) + + result = actualStartTime == null ? + await query.executeAsync([helper.formatDate(fixedStartTime), helper.formatDate(startTime), helper.formatDate(endTime), duration, statusTypeId]) : + await query.executeAsync([helper.formatDate(fixedStartTime), helper.formatDate(startTime), helper.formatDate(endTime), duration, statusTypeId, helper.formatDate(actualStartTime)]) + // await connection.commitTransactionAsync() } catch (e) { logger.error(`Error in 'updatePhase' ${e}, rolling back transaction`)