diff --git a/ReadMe.md b/ReadMe.md index 98cfdfe..86c0264 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -207,6 +207,7 @@ npm run e2e - TBD + ## Verification Refer to the verification document `Verification.md` diff --git a/config/default.js b/config/default.js index 4a1844b..a87a344 100644 --- a/config/default.js +++ b/config/default.js @@ -85,5 +85,7 @@ module.exports = { SYNC_V5_TERM_UUID: process.env.SYNC_V5_TERM_UUID || '317cd8f9-d66c-4f2a-8774-63c612d99cd4', SYNC_V5_WRITE_ENABLED: process.env.SYNC_V5_WRITE_ENABLED === 'true' || false, - TIMEZONE: process.env.TIMEZONE || 'America/New_York' + TIMEZONE: process.env.TIMEZONE || 'America/New_York', + + IGNORED_ORIGINATORS: process.env.IGNORED_ORIGINATORS ? process.env.IGNORED_ORIGINATORS.split(',') : ['legacy-migration-script'] } diff --git a/src/app.js b/src/app.js index e14212a..80b2a3f 100644 --- a/src/app.js +++ b/src/app.js @@ -46,6 +46,14 @@ const dataHandler = (messageSet, topic, partition) => Promise.each(messageSet, a return } + if (_.includes(config.IGNORED_ORIGINATORS, messageJSON.originator)) { + logger.error(`The message originator is in the ignored list. Originator: ${messageJSON.originator}`) + + // commit the message and ignore it + await consumer.commitOffset({ topic, partition, offset: m.offset }) + return + } + // do not trust the message payload // the message.payload will be replaced with the data from the API try { diff --git a/src/constants.js b/src/constants.js index 61a5d88..60ec940 100644 --- a/src/constants.js +++ b/src/constants.js @@ -33,6 +33,7 @@ const createChallengeStatusesMap = { const challengeStatuses = { New: 'New', Draft: 'Draft', + Approved: 'Approved', Canceled: 'Canceled', Active: 'Active', Completed: 'Completed', @@ -43,7 +44,8 @@ const challengeStatuses = { CancelledWinnerUnresponsive: 'Cancelled - Winner Unresponsive', CancelledClientRequest: 'Cancelled - Client Request', CancelledRequirementsInfeasible: 'Cancelled - Requirements Infeasible', - CancelledZeroRegistrations: 'Cancelled - Zero Registrations' + CancelledZeroRegistrations: 'Cancelled - Zero Registrations', + CancelledPaymentFailed: 'Cancelled - Payment Failed' } const PhaseStatusTypes = { diff --git a/src/services/ProcessorService.js b/src/services/ProcessorService.js index c4e0d54..d131180 100644 --- a/src/services/ProcessorService.js +++ b/src/services/ProcessorService.js @@ -16,6 +16,8 @@ const copilotPaymentService = require('./copilotPaymentService') const timelineService = require('./timelineService') const metadataService = require('./metadataService') const paymentService = require('./paymentService') +const { createOrSetNumberOfReviewers } = require('./selfServiceReviewerService') +const { disableTimelineNotifications } = require('./selfServiceNotificationService') /** * Drop and recreate phases in ifx @@ -68,8 +70,10 @@ async function recreatePhases (legacyId, v5Phases, createdBy) { * Sync the information from the v5 phases into legacy * @param {Number} legacyId the legacy challenge ID * @param {Array} v5Phases the v5 phases + * @param {Boolean} isSelfService is the challenge self-service + * @param {String} createdBy the created by */ -async function syncChallengePhases (legacyId, v5Phases) { +async function syncChallengePhases (legacyId, v5Phases, createdBy, isSelfService) { const phaseTypes = await timelineService.getPhaseTypes() const phasesFromIFx = await timelineService.getChallengePhases(legacyId) logger.debug(`Phases from v5: ${JSON.stringify(v5Phases)}`) @@ -104,6 +108,10 @@ async function syncChallengePhases (legacyId, v5Phases) { } else { logger.info(`No v5 Equivalent Found for ${phaseName}`) } + if (isSelfService && phaseName === 'Review') { + // make sure to set the required reviewers to 2 + await createOrSetNumberOfReviewers(phase.project_phase_id, 2, createdBy) + } } // TODO: What about iterative reviews? There can be many for the same challenge. // TODO: handle timeline template updates @@ -216,10 +224,10 @@ async function associateChallengeTerms (v5Terms, legacyChallengeId, createdBy, u const standardTerms = _.find(v5Terms, e => e.id === config.V5_TERMS_STANDARD_ID) const legacyStandardTerms = _.find(legacyTermsArray, e => _.toNumber(e.id) === _.toNumber(config.LEGACY_TERMS_STANDARD_ID)) - // logger.debug(`NDA: ${config.V5_TERMS_NDA_ID} - ${JSON.stringify(nda)}`) - // logger.debug(`Standard Terms: ${config.V5_TERMS_STANDARD_ID} - ${JSON.stringify(standardTerms)}`) - // logger.debug(`Legacy NDA: ${JSON.stringify(legacyNDA)}`) - // logger.debug(`Legacy Standard Terms: ${JSON.stringify(legacyStandardTerms)}`) + logger.debug(`NDA: ${config.V5_TERMS_NDA_ID} - ${JSON.stringify(nda)}`) + logger.debug(`Standard Terms: ${config.V5_TERMS_STANDARD_ID} - ${JSON.stringify(standardTerms)}`) + logger.debug(`Legacy NDA: ${JSON.stringify(legacyNDA)}`) + logger.debug(`Legacy Standard Terms: ${JSON.stringify(legacyStandardTerms)}`) const m2mToken = await helper.getM2MToken() if (standardTerms && standardTerms.id && !legacyStandardTerms) { @@ -254,21 +262,17 @@ async function associateChallengeTerms (v5Terms, legacyChallengeId, createdBy, u */ async function setCopilotPayment (challengeId, legacyChallengeId, prizeSets = [], createdBy, updatedBy, m2mToken) { try { - const copilotPayment = _.get(_.find(prizeSets, p => p.type === config.COPILOT_PAYMENT_TYPE), 'prizes[0].value', null) - if (copilotPayment) { - logger.debug('Fetching challenge copilot...') - const res = await helper.getRequest(`${config.V5_RESOURCES_API_URL}?challengeId=${challengeId}&roleId=${config.COPILOT_ROLE_ID}`, m2mToken) - const [copilotResource] = res.body - if (!copilotResource) { - logger.warn(`Copilot does not exist for challenge ${challengeId} (legacy: ${legacyChallengeId})`) - return - } - logger.debug(`Setting Copilot Payment: ${copilotPayment} for legacyId ${legacyChallengeId} for copilot ${copilotResource.memberId}`) - if (copilotPayment !== null && copilotPayment >= 0) { - await copilotPaymentService.setManualCopilotPayment(legacyChallengeId, createdBy, updatedBy) - } - await copilotPaymentService.setCopilotPayment(legacyChallengeId, copilotPayment, createdBy, updatedBy) + const copilotPayment = _.get(_.find(prizeSets, p => p.type === config.COPILOT_PAYMENT_TYPE), 'prizes[0].value', 0) + logger.debug('Fetching challenge copilot...') + const res = await helper.getRequest(`${config.V5_RESOURCES_API_URL}?challengeId=${challengeId}&roleId=${config.COPILOT_ROLE_ID}`, m2mToken) + const [copilotResource] = res.body + if (!copilotResource) { + logger.warn(`Copilot does not exist for challenge ${challengeId} (legacy: ${legacyChallengeId})`) + return } + logger.debug(`Setting Copilot Payment: ${copilotPayment} for legacyId ${legacyChallengeId} for copilot ${copilotResource.memberId}`) + await copilotPaymentService.setManualCopilotPayment(legacyChallengeId, createdBy, updatedBy) + await copilotPaymentService.setCopilotPayment(legacyChallengeId, copilotPayment, createdBy, updatedBy) } catch (e) { logger.error('Failed to set the copilot payment!') logger.debug(e) @@ -376,7 +380,7 @@ async function parsePayload (payload, m2mToken) { name: payload.name, reviewType: _.get(payload, 'legacy.reviewType', 'INTERNAL'), projectId, - status: payload.status + status: payload.status === constants.challengeStatuses.CancelledPaymentFailed ? constants.challengeStatuses.CancelledFailedScreening : payload.status } if (payload.billingAccountId) { data.billingAccountId = payload.billingAccountId @@ -601,6 +605,7 @@ async function createChallenge (saveDraftContestDTO, challengeUuid, createdByUse // Repost all challenge resource on Kafka so they will get created on legacy by the legacy-challenge-resource-processor await rePostResourcesOnKafka(challengeUuid, m2mToken) await timelineService.enableTimelineNotifications(legacyId, createdByUserId) + await metadataService.createOrUpdateMetadata(legacyId, 9, 'On', createdByUserId) // autopilot return legacyId } @@ -609,8 +614,8 @@ async function createChallenge (saveDraftContestDTO, challengeUuid, createdByUse * @param {Object} message the kafka message */ async function processMessage (message) { - if (_.get(message, 'payload.legacy.pureV5Task')) { - logger.debug(`Challenge ${message.payload.id} is a pure v5 task. Will skip...`) + if (_.get(message, 'payload.legacy.pureV5Task') || _.get(message, 'payload.legacy.pureV5')) { + logger.debug(`Challenge ${message.payload.id} is a pure v5 task or challenge. Will skip...`) return } @@ -619,6 +624,11 @@ async function processMessage (message) { return } + if (message.payload.status === constants.challengeStatuses.Approved) { + logger.debug(`Will skip updating on legacy as status is ${constants.challengeStatuses.Approved}`) + return + } + logger.info(`Processing Kafka Message: ${JSON.stringify(message)}`) const createdByUserHandle = _.get(message, 'payload.createdBy') @@ -640,6 +650,9 @@ async function processMessage (message) { logger.debug('Legacy ID does not exist. Will create...') legacyId = await createChallenge(saveDraftContestDTO, challengeUuid, createdByUserId, message.payload.legacy, m2mToken) await recreatePhases(legacyId, message.payload.phases, updatedByUserId) + if (_.get(message, 'payload.legacy.selfService')) { + await disableTimelineNotifications(legacyId, createdByUserId) // disable + } } let challenge @@ -676,6 +689,8 @@ async function processMessage (message) { logger.info('Activating challenge...') const activated = await activateChallenge(legacyId) logger.info(`Activated! ${JSON.stringify(activated)}`) + // make sure autopilot is on + await metadataService.createOrUpdateMetadata(legacyId, 9, 'On', createdByUserId) // autopilot // Repost all challenge resource on Kafka so they will get created on legacy by the legacy-challenge-resource-processor await rePostResourcesOnKafka(challengeUuid, m2mToken) } @@ -694,7 +709,7 @@ async function processMessage (message) { } if (!_.get(message.payload, 'task.isTask')) { - await syncChallengePhases(legacyId, message.payload.phases) + await syncChallengePhases(legacyId, message.payload.phases, _.get(message, 'payload.legacy.selfService'), createdByUserId) } else { logger.info('Will skip syncing phases as the challenge is a task...') } @@ -720,7 +735,8 @@ processMessage.schema = { reviewType: Joi.string().required(), confidentialityType: Joi.string(), directProjectId: Joi.number(), - forumId: Joi.number().integer().positive() + forumId: Joi.number().integer().positive(), + selfService: Joi.boolean() }).unknown(true), task: Joi.object().keys({ isTask: Joi.boolean().default(false), diff --git a/src/services/selfServiceNotificationService.js b/src/services/selfServiceNotificationService.js new file mode 100644 index 0000000..cf89d77 --- /dev/null +++ b/src/services/selfServiceNotificationService.js @@ -0,0 +1,72 @@ +/** + * timeline notification Service + * Interacts with InformixDB + */ +const util = require('util') +const logger = require('../common/logger') +const helper = require('../common/helper') + +const QUERY_GET_ENTRY = 'SELECT notification_type_id FROM notification WHERE external_ref_id = %d AND project_id = %d' +const QUERY_DELETE = 'DELETE FROM notification WHERE external_ref_id = ? AND project_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) +} + +/** + * Get entry + * @param {Number} legacyId the legacy challenge ID + * @param {String} userId the userId + */ +async function getEntry (legacyId, userId) { + const connection = await helper.getInformixConnection() + let result = null + try { + result = await connection.queryAsync(util.format(QUERY_GET_ENTRY, userId, legacyId)) + } catch (e) { + logger.error(`Error in 'getEntry' ${e}`) + throw e + } finally { + await connection.closeAsync() + } + return result +} + +/** + * Disable timeline notifications + * @param {Number} legacyId the legacy challenge ID + * @param {String} userId the userId + */ +async function disableTimelineNotifications (legacyId, userId) { + const connection = await helper.getInformixConnection() + let result = null + try { + await connection.beginTransactionAsync() + const [existing] = await getEntry(legacyId, userId) + if (existing) { + const query = await prepare(connection, QUERY_DELETE) + result = await query.executeAsync([userId, legacyId]) + } + await connection.commitTransactionAsync() + } catch (e) { + logger.error(`Error in 'disableTimelineNotifications' ${e}, rolling back transaction`) + await connection.rollbackTransactionAsync() + throw e + } finally { + await connection.closeAsync() + } + return result +} + +module.exports = { + getEntry, + disableTimelineNotifications +} diff --git a/src/services/selfServiceReviewerService.js b/src/services/selfServiceReviewerService.js new file mode 100644 index 0000000..5bf3cad --- /dev/null +++ b/src/services/selfServiceReviewerService.js @@ -0,0 +1,84 @@ +/** + * Number of reviewers Service + * Interacts with InformixDB + */ +const util = require('util') +const logger = require('../common/logger') +const helper = require('../common/helper') + +const QUERY_GET_ENTRY = 'SELECT parameter FROM phase_criteria WHERE project_phase_id = %d' +const QUERY_CREATE = 'INSERT INTO phase_criteria (project_phase_id, phase_criteria_type_id, parameter, create_user, create_date, modify_user, modify_date) VALUES (?, 6, ?, ?, CURRENT, ?, CURRENT)' +const QUERY_UPDATE = 'UPDATE phase_criteria SET parameter = ?, modify_user = ?, modify_date = CURRENT WHERE project_phase_id = ?' +const QUERY_DELETE = 'DELETE FROM phase_criteria WHERE project_phase_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) +} + +/** + * Get entry + * @param {Number} phaseId the phase ID + */ +async function getEntry (phaseId) { + // logger.debug(`Getting Groups for Challenge ${challengeLegacyId}`) + const connection = await helper.getInformixConnection() + let result = null + try { + result = await connection.queryAsync(util.format(QUERY_GET_ENTRY, phaseId)) + } catch (e) { + logger.error(`Error in 'getEntry' ${e}`) + throw e + } finally { + await connection.closeAsync() + } + return result +} + +/** + * Enable timeline notifications + * @param {Number} phaseId the legacy challenge ID + * @param {Number} typeId the type ID + * @param {Any} value the value + * @param {String} createdBy the created by + */ +async function createOrSetNumberOfReviewers (phaseId, value, createdBy) { + const connection = await helper.getInformixConnection() + let result = null + try { + await connection.beginTransactionAsync() + const [existing] = await getEntry(phaseId) + if (existing) { + if (value) { + const query = await prepare(connection, QUERY_UPDATE) + result = await query.executeAsync([value, createdBy, phaseId]) + } else { + const query = await prepare(connection, QUERY_DELETE) + result = await query.executeAsync([phaseId, value]) + } + } else { + const query = await prepare(connection, QUERY_CREATE) + result = await query.executeAsync([phaseId, value, createdBy, createdBy]) + } + await connection.commitTransactionAsync() + } catch (e) { + logger.error(`Error in 'createOrSetNumberOfReviewers' ${e}, rolling back transaction`) + await connection.rollbackTransactionAsync() + throw e + } finally { + await connection.closeAsync() + } + return result +} + +module.exports = { + getEntry, + createOrSetNumberOfReviewers +}