From 80350165116fc453ac789fd8ca7a29e365e6099f Mon Sep 17 00:00:00 2001 From: Cagdas U Date: Tue, 27 Jul 2021 13:51:42 +0300 Subject: [PATCH 01/50] feat: polish release - 2 * Accept `hoursPerWeek` for jobs, when creating a team. * Fix `getSkillIdsByNames` to return ids in the exact same order of names. * Include skill ids in `getSkillsByJobDescription` response Addresses https://github.com/topcoder-platform/taas-app/issues/400, https://github.com/topcoder-platform/taas-app/issues/407 --- docs/swagger.yaml | 8 ++++++++ src/services/TeamService.js | 16 ++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6eb3ef5d..81e140b7 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3655,6 +3655,9 @@ components: schemas: SkillItem: properties: + id: + type: string + format: uuid tag: type: string example: "Java" @@ -5440,6 +5443,11 @@ components: example: 10 minimum: 1 description: "The number of needed resources" + hoursPerWeek: + type: integer + example: 40 + minimum: 1 + description: "The amount of working hours per week" durationWeeks: type: integer example: 5 diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 2ffd9c2d..3d1f390b 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -874,10 +874,12 @@ async function getSkillsByJobDescription (data) { }) }) foundSkills = _.uniq(foundSkills) + const skillIds = await getSkillIdsByNames(foundSkills) // apply desired template - _.each(foundSkills, skill => { + _.each(foundSkills, (skillTag, idx) => { result.push({ - tag: skill, + id: skillIds[idx], + tag: skillTag, type: 'taas_skill', source: 'taas-jd-parser' }) @@ -933,12 +935,12 @@ getSkillNamesByIds.schema = Joi.object() * @returns {Array} the array of skill ids */ async function getSkillIdsByNames (skills) { - const result = await helper.getAllTopcoderSkills({ name: _.join(skills, ',') }) + const tcSkills = await helper.getAllTopcoderSkills({ name: _.join(skills, ',') }) // endpoint returns the partial matched skills // we need to filter by exact match case insensitive - const filteredSkills = _.filter(result, tcSkill => _.some(skills, skill => _.toLower(skill) === _.toLower(tcSkill.name))) - const skillIds = _.map(filteredSkills, 'id') - return skillIds + // const filteredSkills = _.filter(result, tcSkill => _.some(skills, skill => _.toLower(skill) === _.toLower(tcSkill.name))) + const matchedSkills = _.map(skills, skillTag => tcSkills.find(tcSkill => _.toLower(skillTag) === _.toLower(tcSkill.name))) + return _.map(matchedSkills, 'id') } getSkillIdsByNames.schema = Joi.object() @@ -1051,6 +1053,7 @@ async function createTeam (currentUser, data) { numPositions: position.numberOfResources, rateType: position.rateType, workload: position.workload, + hoursPerWeek: position.hoursPerWeek, skills: roleSearchRequest.skills, description: roleSearchRequest.jobDescription, roleIds: [roleSearchRequest.roleId], @@ -1083,6 +1086,7 @@ createTeam.schema = Joi.object() startMonth: Joi.date(), rateType: Joi.rateType().default('weekly'), workload: Joi.workload().default('full-time'), + hoursPerWeek: Joi.number().integer().positive(), resourceType: Joi.string() }).required() ).required() From c1b0578cfe1543ac06c1f64381ba50555d0c3b8c Mon Sep 17 00:00:00 2001 From: yoution Date: Wed, 28 Jul 2021 13:58:14 +0800 Subject: [PATCH 02/50] Send Weekly Surveys --- app.js | 4 +- config/default.js | 10 + docs/swagger.yaml | 44 ++++ ...21-07-26-add-send-weekly-survery-fields.js | 43 ++++ package.json | 1 + .../data/updateWorkPeriodSentSurveyField.js | 68 +++++ src/common/helper.js | 10 + src/common/surveyMonkey.js | 237 ++++++++++++++++++ src/models/ResourceBooking.js | 6 + src/models/WorkPeriod.js | 20 ++ src/services/ResourceBookingService.js | 28 ++- src/services/SurveyService.js | 134 ++++++++++ src/services/WorkPeriodService.js | 17 +- 13 files changed, 613 insertions(+), 9 deletions(-) create mode 100644 migrations/2021-07-26-add-send-weekly-survery-fields.js create mode 100644 scripts/data/updateWorkPeriodSentSurveyField.js create mode 100644 src/common/surveyMonkey.js create mode 100644 src/services/SurveyService.js diff --git a/app.js b/app.js index e6d79c69..08395ffc 100644 --- a/app.js +++ b/app.js @@ -14,6 +14,7 @@ const logger = require('./src/common/logger') const eventHandlers = require('./src/eventHandlers') const interviewService = require('./src/services/InterviewService') const { processScheduler } = require('./src/services/PaymentSchedulerService') +const { sendSurveys } = require('./src/services/SurveyService') // setup express app const app = express() @@ -98,7 +99,8 @@ const server = app.listen(app.get('port'), () => { eventHandlers.init() // schedule updateCompletedInterviews to run every hour schedule.scheduleJob('0 0 * * * *', interviewService.updateCompletedInterviews) - + // schedule sendSurveys + schedule.scheduleJob(config.WEEKLY_SURVEY.CRON, sendSurveys) // schedule payment processing schedule.scheduleJob(config.PAYMENT_PROCESSING.CRON, processScheduler) }) diff --git a/config/default.js b/config/default.js index cb589290..4675ff09 100644 --- a/config/default.js +++ b/config/default.js @@ -180,6 +180,16 @@ module.exports = { INTERNAL_MEMBER_GROUPS: process.env.INTERNAL_MEMBER_GROUPS || ['20000000', '20000001', '20000003', '20000010', '20000015'], // Topcoder skills cache time in minutes TOPCODER_SKILLS_CACHE_TIME: process.env.TOPCODER_SKILLS_CACHE_TIME || 60, + // weekly survey scheduler config + WEEKLY_SURVEY: { + CRON: process.env.WEEKLY_SURVEY_CRON || '0 1 * * 7', + BASE_URL: process.env.WEEKLY_SURVEY_BASE_URL || 'https://api.surveymonkey.net/v3/surveys', + JWT_TOKEN: process.env.WEEKLY_SURVEY_JWT_TOKEN || '', + SURVEY_ID: process.env.WEEKLY_SURVEY_SURVEY_ID || '', + SURVEY_MASTER_COLLECTOR_ID: process.env.WEEKLY_SURVEY_SURVEY_MASTER_COLLECTOR_ID || '', + SURVEY_MASTER_MESSAGE_ID: process.env.WEEKLY_SURVEY_SURVEY_MASTER_MESSAGE_ID || '', + SURVEY_CONTACT_GROUP_ID: process.env.WEEKLY_SURVEY_SURVEY_CONTACT_GROUP_ID || '' + }, // payment scheduler config PAYMENT_PROCESSING: { // switch off actual API calls in Payment Scheduler diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6eb3ef5d..f0f5a0e5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -4470,6 +4470,10 @@ components: format: float example: 13 description: "The member rate." + sendWeeklySurvey: + type: boolean + example: true, + description: "whether we should send weekly survey to this ResourceBooking or no" customerRate: type: integer format: float @@ -4527,6 +4531,10 @@ components: format: uuid example: "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a" description: "The external id." + sendWeeklySurvey: + type: boolean + example: true, + description: "whether we should send weekly survey to this ResourceBooking or no" jobId: type: string format: uuid @@ -4584,6 +4592,10 @@ components: format: float example: 13.23 description: "The member rate." + sendWeeklySurvey: + type: boolean + example: true, + description: "whether we should send weekly survey to this ResourceBooking or no" customerRate: type: number format: float @@ -4620,6 +4632,22 @@ components: type: string format: uuid description: "The resource booking id." + sentSurvey: + type: boolean + example: true + description: "whether we've already sent a survey for this WorkPeriod of no" + sentSurveyError: + description: "error details if error happened during sending survey" + type: object + properties: + errorMessage: + type: string + example: "error message" + description: "The error message" + errorCode: + type: integer + example: 429 + description: "HTTP code of error" userHandle: type: string example: "eisbilir" @@ -4695,6 +4723,22 @@ components: type: integer example: 2 description: "The count of the days worked for that work period." + sentSurvey: + type: boolean + example: true + description: "whether we've already sent a survey for this WorkPeriod of no" + sentSurveyError: + description: "error details if error happened during sending survey" + type: object + properties: + errorMessage: + type: string + example: "error message" + description: "The error message" + errorCode: + type: integer + example: 429 + description: "HTTP code of error" WorkPeriodPayment: required: - id diff --git a/migrations/2021-07-26-add-send-weekly-survery-fields.js b/migrations/2021-07-26-add-send-weekly-survery-fields.js new file mode 100644 index 00000000..025d79d9 --- /dev/null +++ b/migrations/2021-07-26-add-send-weekly-survery-fields.js @@ -0,0 +1,43 @@ +const config = require('config') + +module.exports = { + up: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.addColumn({ tableName: 'resource_bookings', schema: config.DB_SCHEMA_NAME }, 'send_weekly_survey', + { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true }, + { transaction }) + await queryInterface.addColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'sent_survey', + { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'sent_survey_error', + { + type: Sequelize.JSONB({ + errorCode: { + field: 'error_code', + type: Sequelize.INTEGER, + }, + errorMessage: { + field: 'error_message', + type: Sequelize.STRING(255) + }, + }), allowNull: true }, { transaction }) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + }, + down: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.removeColumn({ tableName: 'resource_bookings', schema: config.DB_SCHEMA_NAME }, 'send_weekly_survey', { transaction }) + await queryInterface.removeColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'send_survey', { transaction }) + await queryInterface.removeColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'sent_survey_error', { transaction } ) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + }, +} diff --git a/package.json b/package.json index 17c1887b..70b1913c 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "index:roles": "node scripts/es/reIndexRoles.js", "data:export": "node scripts/data/exportData.js", "data:import": "node scripts/data/importData.js", + "data:workperiod": "node scripts/data/updateWorkPeriodSentSurveyField.js", "migrate": "npx sequelize db:migrate", "migrate:undo": "npx sequelize db:migrate:undo", "test": "mocha test/unit/*.test.js --timeout 30000 --require test/prepare.js --exit", diff --git a/scripts/data/updateWorkPeriodSentSurveyField.js b/scripts/data/updateWorkPeriodSentSurveyField.js new file mode 100644 index 00000000..e641b177 --- /dev/null +++ b/scripts/data/updateWorkPeriodSentSurveyField.js @@ -0,0 +1,68 @@ +/* + * update WorkPeriod field `sentSurvey=true` + */ +const _ = require('lodash') +const moment = require('moment') +const logger = require('../../src/common/logger') +const { Op } = require('sequelize') +const models = require('../../src/models') + +const ResourceBooking = models.ResourceBooking +const WorkPeriod = models.WorkPeriod + +async function updateWorkPeriod () { + const transaction = await models.sequelize.transaction() + try { + // Start a transaction + const queryCriteria = { + attributes: ['sendWeeklySurvey', 'id'], + include: [{ + as: 'workPeriods', + model: WorkPeriod, + required: true, + where: { + [Op.and]: [ + { sentSurveyError: null }, + { sentSurvey: false }, + { paymentStatus: ['completed'] }, + { endDate: { [Op.lte]: moment().subtract(7, 'days').format('YYYY-MM-DD') } } + ] + } + }], + where: { + [Op.and]: [{ sendWeeklySurvey: true }] + }, + transaction + } + + const resourceBookings = await ResourceBooking.findAll(queryCriteria) + + _.forEach(resourceBookings, r => { + _.forEach(r.workPeriods, async w => { + // await w.update({sentSurvey: true}, {transaction: transaction} ) + await w.update({ sentSurvey: true }) + }) + }) + + // commit transaction only if all things went ok + logger.info({ + component: 'importData', + message: 'committing transaction to database...' + }) + await transaction.commit() + } catch (error) { + // logger.error({ + // component: 'importData', + // message: `Error while writing data of model: WorkPeriod` + // }) + // rollback all insert operations + if (transaction) { + logger.info({ + component: 'importData', + message: 'rollback database transaction...' + }) + transaction.rollback() + } + } +} +updateWorkPeriod() diff --git a/src/common/helper.js b/src/common/helper.js index 7f9625be..319eb86a 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -176,6 +176,7 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { endDate: { type: 'date', format: 'yyyy-MM-dd' }, memberRate: { type: 'float' }, customerRate: { type: 'float' }, + sendWeeklySurvey: { type: 'boolean' }, rateType: { type: 'keyword' }, billingAccountId: { type: 'integer', null_value: 0 }, workPeriods: { @@ -189,6 +190,14 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { }, projectId: { type: 'integer' }, userId: { type: 'keyword' }, + sentSurvey: { type: 'boolean' }, + sentSurveyError: { + type: 'nested', + properties: { + errorCode: { type: 'integer' }, + errorMessage: { type: 'keyword' } + } + }, startDate: { type: 'date', format: 'yyyy-MM-dd' }, endDate: { type: 'date', format: 'yyyy-MM-dd' }, daysWorked: { type: 'integer' }, @@ -2012,6 +2021,7 @@ async function getMembersSuggest (fragment) { } module.exports = { + encodeQueryString, getParamFromCliArgs, promptUser, sleep, diff --git a/src/common/surveyMonkey.js b/src/common/surveyMonkey.js new file mode 100644 index 00000000..c3a73499 --- /dev/null +++ b/src/common/surveyMonkey.js @@ -0,0 +1,237 @@ +/* + * surveymonkey api + * + */ + +const logger = require('./logger') +const config = require('config') +const _ = require('lodash') +const request = require('superagent') +const moment = require('moment') +const { encodeQueryString } = require('./helper') +/** + * This code uses several environment variables + * + * WEEKLY_SURVEY_SURVEY_CONTACT_GROUP_ID - the ID of contacts list which would be used to store all the contacts, + * see https://developer.surveymonkey.com/api/v3/#contact_lists-id + * WEEKLY_SURVEY_SURVEY_MASTER_COLLECTOR_ID - the ID of master collector - this collector should be created manually, + * and all other collectors would be created by copying this master collector. + * This is needed so we can make some config inside master collector which would + * be applied to all collectors. + * WEEKLY_SURVEY_SURVEY_MASTER_MESSAGE_ID - the ID of master message - similar to collector, this message would be created manually + * and then script would create copies of this message to use the same config. + */ + +const localLogger = { + debug: (message, context) => logger.debug({ component: 'SurveyMonkeyAPI', context, message }), + error: (message, context) => logger.error({ component: 'SurveyMonkeyAPI', context, message }), + info: (message, context) => logger.info({ component: 'SurveyMonkeyAPI', context, message }) +} + +function getRemainingRequestCountMessge (response) { + return `today has sent ${response.header['x-ratelimit-app-global-day-limit'] - response.header['x-ratelimit-app-global-day-remaining']} requests` +} + +function getErrorMessage (e) { + return { + errorCode: _.get(e, 'response.body.error.http_status_code', 400), + errorMessage: _.get(e, 'response.body.error.message', 'error message') + } +} + +function getSingleItem (lst, errorMessage) { + if (lst.length === 0) { + return null + } + + if (lst.length > 1) { + throw new Error(errorMessage) + } + + return lst[0].id +} + +/* + * get collector name + * + * format `Week Ending yyyy-nth(weeks)` + */ +function getCollectorName (dt) { + return 'Week Ending ' + moment(dt).year() + '-' + moment(dt).format('ww') +} + +/* + * search collector by name + */ +async function searchCollector (collectorName) { + const url = `${config.WEEKLY_SURVEY.BASE_URL}/surveys/${config.WEEKLY_SURVEY.SURVEY_ID}/collectors?${encodeQueryString({ name: collectorName })}` + try { + const response = await request + .get(url) + .set('Authorization', `Bearer ${config.WEEKLY_SURVEY.JWT_TOKEN}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + + localLogger.info(`URL ${url}, ${getRemainingRequestCountMessge(response)}`, 'searchCollector') + + return getSingleItem(response.body.data, 'More than 1 collector found by name ' + collectorName) + } catch (e) { + localLogger.error(`URL ${url} ${getErrorMessage(e)}, ${getRemainingRequestCountMessge(e.response)}`, 'searchCollector') + throw getErrorMessage(e) + } +} + +/* + * create a named collector if not created + * else return the collectId of the named collector + */ +async function createCollector (collectorName) { + let collectorID = await searchCollector(collectorName) + if (collectorID) { + return collectorID + } + + collectorID = await cloneCollector() + await renameCollector(collectorID, collectorName) + + return collectorID +} + +/* + * clone collector from MASTER_COLLECTOR + */ +async function cloneCollector () { + const body = { from_collector_id: `${config.WEEKLY_SURVEY.SURVEY_MASTER_COLLECTOR_ID}` } + const url = `${config.WEEKLY_SURVEY.BASE_URL}/surveys/${config.WEEKLY_SURVEY.SURVEY_ID}/collectors` + try { + const response = await request + .post(url) + .set('Authorization', `Bearer ${config.WEEKLY_SURVEY.JWT_TOKEN}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .send(body) + localLogger.info(`URL ${url}, ${getRemainingRequestCountMessge(response)}`, 'cloneCollector') + return response.body.id + } catch (e) { + localLogger.error(`URL ${url} ${JSON.stringify(getErrorMessage(e))}, ${getRemainingRequestCountMessge(e.response)}`, 'cloneCollector') + throw getErrorMessage(e) + } +} + +/* + * rename collector + */ +async function renameCollector (collectorId, name) { + const body = { name: name } + // http.patch(BASE_URL + '/collectors/' + collectorId, body); + const url = `${config.WEEKLY_SURVEY.BASE_URL}/collectors/${collectorId}` + try { + const response = await request + .patch(url) + .set('Authorization', `Bearer ${config.WEEKLY_SURVEY.JWT_TOKEN}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .send(body) + localLogger.info(`URL ${url}, ${getRemainingRequestCountMessge(response)}`, 'renameCollector') + } catch (e) { + localLogger.error(`URL ${url} ${JSON.stringify(getErrorMessage(e))}, ${getRemainingRequestCountMessge(e.response)}`, 'renameCollector') + throw getErrorMessage(e) + } +} + +/* + * create message + */ +async function createMessage (collectorId) { + const body = { + from_collector_id: `${config.WEEKLY_SURVEY.SURVEY_MASTER_COLLECTOR_ID}`, + from_message_id: `${config.WEEKLY_SURVEY.SURVEY_MASTER_MESSAGE_ID}` + } + // response = http.post(BASE_URL + '/collectors/' + collectorId + '/messages', body); + const url = `${config.WEEKLY_SURVEY.BASE_URL}/collectors/${collectorId}/messages` + try { + const response = await request + .post(url) + .set('Authorization', `Bearer ${config.WEEKLY_SURVEY.JWT_TOKEN}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .send(body) + localLogger.info(`URL ${url}, ${getRemainingRequestCountMessge(response)}`, 'createMessage') + return response.body.id + } catch (e) { + localLogger.error(`URL ${url} ${JSON.stringify(getErrorMessage(e))}, ${getRemainingRequestCountMessge(e.response)}`, 'createMessage') + throw getErrorMessage(e) + } +} + +/** + * Add Contact Email to List for sending a survey + */ +async function upsertContactInSurveyMonkey (list) { + list = _.filter(list, p => p.email) + if (!list.length) { + return [] + } + const body = { + contacts: list + } + const url = `${config.WEEKLY_SURVEY.BASE_URL}/contact_lists/${config.WEEKLY_SURVEY.SURVEY_CONTACT_GROUP_ID}/contacts/bulk` + try { + const response = await request + .post(url) + .set('Authorization', `Bearer ${config.WEEKLY_SURVEY.JWT_TOKEN}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .send(body) + + localLogger.info(`URL ${url}, ${getRemainingRequestCountMessge(response)}`, 'upsertContactInSurveyMonkey') + return _.concat(response.body.existing, response.body.succeeded) + } catch (e) { + localLogger.error(`URL ${url} ${JSON.stringify(getErrorMessage(e))}, ${getRemainingRequestCountMessge(e.response)}`, 'createMessage') + throw getErrorMessage(e) + } +} + +async function addContactsToSurvey (collectorId, messageId, contactIds) { + const url = `${config.WEEKLY_SURVEY.BASE_URL}/collectors/${collectorId}/messages/${messageId}/recipients/bulk` + const body = { contact_ids: _.map(contactIds, 'id') } + try { + const response = await request + .post(url) + .set('Authorization', `Bearer ${config.WEEKLY_SURVEY.JWT_TOKEN}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .send(body) + localLogger.info(`URL ${url}, ${getRemainingRequestCountMessge(response)}`, 'addContactsToSurvey') + return response.body.id + } catch (e) { + localLogger.error(`URL ${url} ${JSON.stringify(getErrorMessage(e))}, ${getRemainingRequestCountMessge(e.response)}`, 'addContactsToSurvey') + throw getErrorMessage(e) + } +} + +async function sendSurveyAPI (collectorId, messageId) { + const url = `${config.WEEKLY_SURVEY.BASE_URL}/collectors/${collectorId}/messages/${messageId}/send` + try { + const response = await request + .post(url) + .set('Authorization', `Bearer ${config.WEEKLY_SURVEY.JWT_TOKEN}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .send({}) + localLogger.info(`URL ${url}, ${getRemainingRequestCountMessge(response)}`, 'sendSurveyAPI') + return response.body.id + } catch (e) { + localLogger.error(`URL ${url} ${JSON.stringify(getErrorMessage(e))}, ${getRemainingRequestCountMessge(e.response)}`, 'sendSurveyAPI') + throw getErrorMessage(e) + } +} + +module.exports = { + getCollectorName, + createCollector, + createMessage, + upsertContactInSurveyMonkey, + addContactsToSurvey, + sendSurveyAPI +} diff --git a/src/models/ResourceBooking.js b/src/models/ResourceBooking.js index 580e6e96..21a222f1 100644 --- a/src/models/ResourceBooking.js +++ b/src/models/ResourceBooking.js @@ -122,6 +122,12 @@ module.exports = (sequelize) => { type: Sequelize.STRING(255), allowNull: false }, + sendWeeklySurvey: { + field: 'send_weekly_survey', + type: Sequelize.BOOLEAN, + defaultValue: true, + allowNull: false + }, billingAccountId: { field: 'billing_account_id', type: Sequelize.BIGINT diff --git a/src/models/WorkPeriod.js b/src/models/WorkPeriod.js index 720e4870..0204d9be 100644 --- a/src/models/WorkPeriod.js +++ b/src/models/WorkPeriod.js @@ -56,6 +56,26 @@ module.exports = (sequelize) => { type: Sequelize.UUID, allowNull: false }, + sentSurvey: { + field: 'send_survey', + type: Sequelize.BOOLEAN, + defaultValue: false, + allowNull: false + }, + sentSurveyError: { + field: 'sent_survey_error', + allowNull: true, + type: Sequelize.JSONB({ + errorCode: { + field: 'error_code', + type: Sequelize.INTEGER + }, + errorMessage: { + field: 'error_message', + type: Sequelize.STRING(255) + } + }) + }, userHandle: { field: 'user_handle', type: Sequelize.STRING(50), diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 46d2fe62..e141f0c5 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -354,6 +354,7 @@ createResourceBooking.schema = Joi.object().keys({ projectId: Joi.number().integer().required(), userId: Joi.string().uuid().required(), jobId: Joi.string().uuid().allow(null), + sendWeeklySurvey: Joi.boolean().default(true), startDate: Joi.date().format('YYYY-MM-DD').allow(null), endDate: Joi.date().format('YYYY-MM-DD').when('startDate', { is: Joi.exist(), @@ -427,6 +428,7 @@ partiallyUpdateResourceBooking.schema = Joi.object().keys({ memberRate: Joi.number().allow(null), customerRate: Joi.number().allow(null), rateType: Joi.rateType(), + sendWeeklySurvey: Joi.boolean().allow(null), billingAccountId: Joi.number().allow(null) }).required() }).required() @@ -466,6 +468,7 @@ fullyUpdateResourceBooking.schema = Joi.object().keys({ customerRate: Joi.number().allow(null).default(null), rateType: Joi.rateType().required(), status: Joi.resourceBookingStatus().required(), + sendWeeklySurvey: Joi.boolean().allow(null), billingAccountId: Joi.number().allow(null).default(null) }).required() }).required() @@ -546,6 +549,10 @@ async function searchResourceBookings (currentUser, criteria, options) { if (!criteria.sortOrder) { criteria.sortOrder = 'desc' } + + if (_.has(criteria, 'workPeriods.sentSurveyError') && !criteria['workPeriods.sentSurveyError']) { + criteria['workPeriods.sentSurveyError'] = null + } // this option to return data from DB is only for internal usage, and it cannot be passed from the endpoint if (!options.returnFromDB) { try { @@ -590,7 +597,7 @@ async function searchResourceBookings (currentUser, criteria, options) { } esQuery.body.sort.push(sort) // Apply ResourceBooking filters - _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId', 'billingAccountId']), (value, key) => { + _.each(_.pick(criteria, ['sendWeeklySurvey', 'status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId', 'billingAccountId']), (value, key) => { esQuery.body.query.bool.must.push({ term: { [key]: { @@ -626,7 +633,7 @@ async function searchResourceBookings (currentUser, criteria, options) { }) } // Apply WorkPeriod and WorkPeriodPayment filters - const workPeriodFilters = _.pick(criteria, ['workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle']) + const workPeriodFilters = _.pick(criteria, ['workPeriods.sentSurveyError', 'workPeriods.sentSurvey', 'workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle']) const workPeriodPaymentFilters = _.pick(criteria, ['workPeriods.payments.status', 'workPeriods.payments.days']) if (!_.isEmpty(workPeriodFilters) || !_.isEmpty(workPeriodPaymentFilters)) { const workPeriodsMust = [] @@ -637,7 +644,7 @@ async function searchResourceBookings (currentUser, criteria, options) { [key]: value } }) - } else { + } else if (key !== 'workPeriods.sentSurveyError') { workPeriodsMust.push({ term: { [key]: { @@ -666,6 +673,7 @@ async function searchResourceBookings (currentUser, criteria, options) { } }) } + esQuery.body.query.bool.must.push({ nested: { path: 'workPeriods', @@ -688,7 +696,9 @@ async function searchResourceBookings (currentUser, criteria, options) { r.workPeriods = _.filter(r.workPeriods, wp => { return _.every(_.omit(workPeriodFilters, 'workPeriods.userHandle'), (value, key) => { key = key.split('.')[1] - if (key === 'paymentStatus') { + if (key === 'sentSurveyError' && !workPeriodFilters['workPeriods.sentSurveyError']) { + return !wp[key] + } else if (key === 'paymentStatus') { return _.includes(value, wp[key]) } else { return wp[key] === value @@ -723,7 +733,7 @@ async function searchResourceBookings (currentUser, criteria, options) { logger.info({ component: 'ResourceBookingService', context: 'searchResourceBookings', message: 'fallback to DB query' }) const filter = { [Op.and]: [] } // Apply ResourceBooking filters - _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId']), (value, key) => { + _.each(_.pick(criteria, ['sendWeeklySurvey', 'status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId']), (value, key) => { filter[Op.and].push({ [key]: value }) }) if (!_.isUndefined(criteria.billingAccountId)) { @@ -773,7 +783,7 @@ async function searchResourceBookings (currentUser, criteria, options) { queryCriteria.include[0].attributes = { exclude: _.map(queryOpt.excludeWP, f => _.split(f, '.')[1]) } } // Apply WorkPeriod filters - _.each(_.pick(criteria, ['workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.paymentStatus']), (value, key) => { + _.each(_.pick(criteria, ['workPeriods.sentSurveyError', 'workPeriods.sentSurvey', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.paymentStatus']), (value, key) => { key = key.split('.')[1] queryCriteria.include[0].where[Op.and].push({ [key]: value }) }) @@ -869,6 +879,7 @@ searchResourceBookings.schema = Joi.object().keys({ Joi.string(), Joi.array().items(Joi.number().integer()) ), + sendWeeklySurvey: Joi.boolean(), billingAccountId: Joi.number().integer(), 'workPeriods.paymentStatus': Joi.alternatives( Joi.string(), @@ -891,6 +902,11 @@ searchResourceBookings.schema = Joi.object().keys({ return value }), 'workPeriods.userHandle': Joi.string(), + 'workPeriods.sentSurvey': Joi.boolean(), + 'workPeriods.sentSurveyError': Joi.object().keys({ + errorCode: Joi.number().integer().min(0), + errorMessage: Joi.string() + }).allow('').optional(), 'workPeriods.isFirstWeek': Joi.when(Joi.ref('workPeriods.startDate', { separator: false }), { is: Joi.exist(), then: Joi.boolean().default(false), diff --git a/src/services/SurveyService.js b/src/services/SurveyService.js new file mode 100644 index 00000000..0b80db4f --- /dev/null +++ b/src/services/SurveyService.js @@ -0,0 +1,134 @@ +const _ = require('lodash') +const logger = require('../common/logger') +const { searchResourceBookings } = require('./ResourceBookingService') +const { partiallyUpdateWorkPeriod } = require('./WorkPeriodService') +const { Scopes } = require('../../app-constants') +const { getUserById, getMemberDetailsByHandle } = require('../common/helper') +const { getCollectorName, createCollector, createMessage, upsertContactInSurveyMonkey, addContactsToSurvey, sendSurveyAPI } = require('../common/surveyMonkey') + +const resourceBookingCache = {} +const contactIdToWorkPeriodIdMap = {} +const emailToWorkPeriodIdMap = {} + +/** + * Scheduler process entrance + */ +async function sendSurveys () { + const currentUser = { + isMachine: true, + scopes: [Scopes.ALL_WORK_PERIOD, Scopes.ALL_WORK_PERIOD_PAYMENT] + } + + const criteria = { + fields: 'workPeriods,userId,id,sendWeeklySurvey', + sendWeeklySurvey: true, + 'workPeriods.paymentStatus': 'completed', + 'workPeriods.sentSurvey': false, + 'workPeriods.sentSurveyError': '', + jobIds: [], + page: 1, + perPage: 1000 + } + + const options = { + returnAll: false, + returnFromDB: false + } + try { + let resourceBookings = await searchResourceBookings(currentUser, criteria, options) + resourceBookings = resourceBookings.result + + logger.info({ component: 'SurveyService', context: 'sendSurvey', message: 'load workPeriod successfullly' }) + + const workPeriods = _.flatten(_.map(resourceBookings, 'workPeriods')) + + const collectors = {} + + // for each WorkPeriod make sure to creat a collector (one per week) + // so several WorkPeriods for the same week would be included into on collector + // and gather contacts (members) from each WorkPeriods + for (const workPeriod of workPeriods) { + // await partiallyUpdateWorkPeriod(currentUser, workPeriod.id, {sentSurvey: true}) + // await partiallyUpdateWorkPeriod(currentUser, workPeriod.id, {sentSurveyError: {errorCode: 23, errorMessage: "sf"}}) + try { + const collectorName = getCollectorName(workPeriod.endDate) + + // create collector and message for each week if not yet + if (!collectors[collectorName]) { + const collectorId = await createCollector(collectorName) + const messageId = await createMessage(collectorId) + // create map + contactIdToWorkPeriodIdMap[collectorName] = {} + emailToWorkPeriodIdMap[collectorName] = {} + collectors[collectorName] = { + collectorId, + messageId, + contacts: [] + } + } + + const resourceBooking = _.find(resourceBookings, (r) => r.id === workPeriod.resourceBookingId) + const userInfo = {} + if (!resourceBookingCache[resourceBooking.userId]) { + let user = await getUserById(resourceBooking.userId) + if (!user.email && user.handle) { + user = await getMemberDetailsByHandle(user.handle) + } + if (user.email) { + userInfo.email = user.email + if (user.firstName) { + userInfo.first_name = user.firstName + } + if (user.lastName) { + userInfo.last_name = user.lastName + } + resourceBookingCache[resourceBooking.userId] = userInfo + } + } + emailToWorkPeriodIdMap[collectorName][resourceBookingCache[resourceBooking.userId].email] = workPeriod.id + // resourceBookingCache[resourceBooking.userId].workPeriodId = workPeriod.id + collectors[collectorName].contacts.push(resourceBookingCache[resourceBooking.userId]) + } catch (e) { + await partiallyUpdateWorkPeriod(currentUser, workPeriod.id, { sentSurveyError: e }) + } + } + // add contacts + for (const collectorName in collectors) { + const collector = collectors[collectorName] + collectors[collectorName].contacts = await upsertContactInSurveyMonkey(collector.contacts) + + for (const contact of collectors[collectorName].contacts) { + contactIdToWorkPeriodIdMap[collectorName][contact.id] = emailToWorkPeriodIdMap[collectorName][contact.email] + } + } + + // send surveys + for (const collectorName in collectors) { + const collector = collectors[collectorName] + try { + await addContactsToSurvey( + collector.collectorId, + collector.messageId, + collector.contacts + ) + await sendSurveyAPI(collector.collectorId, collector.messageId) + + for (const contactId in contactIdToWorkPeriodIdMap[collectorName]) { + await partiallyUpdateWorkPeriod(currentUser, contactIdToWorkPeriodIdMap[collectorName][contactId], { sentSurvey: true }) + } + } catch (e) { + for (const contactId in contactIdToWorkPeriodIdMap[collectorName]) { + await partiallyUpdateWorkPeriod(currentUser, contactIdToWorkPeriodIdMap[collectorName][contactId], { sentSurveyError: e }) + } + } + } + + logger.info({ component: 'SurveyService', context: 'sendSurvey', message: 'send survey successfullly' }) + } catch (e) { + logger.error({ component: 'SurveyService', context: 'sendSurvey', message: 'Error : ' + e.message }) + } +} + +module.exports = { + sendSurveys +} diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index 8d018fb0..d28174a2 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -241,6 +241,7 @@ createWorkPeriod.schema = Joi.object().keys({ resourceBookingId: Joi.string().uuid().required(), startDate: Joi.workPeriodStartDate(), endDate: Joi.workPeriodEndDate(), + sentSurvey: Joi.boolean().default(true), daysWorked: Joi.number().integer().min(0).max(5).required(), daysPaid: Joi.number().default(0).forbidden(), paymentTotal: Joi.number().default(0).forbidden(), @@ -277,7 +278,9 @@ async function updateWorkPeriod (currentUser, id, data) { throw new errors.BadRequestError(`Maximum allowed daysWorked is (${thisWeek.daysWorked})`) } data.paymentStatus = helper.calculateWorkPeriodPaymentStatus(_.assign({}, oldValue, data)) - data.updatedBy = await helper.getUserId(currentUser.userId) + if (!currentUser.isMachine) { + data.updatedBy = await helper.getUserId(currentUser.userId) + } const updated = await workPeriod.update(data) const updatedDataWithoutPayments = _.omit(updated.toJSON(), ['payments']) const oldValueWithoutPayments = _.omit(oldValue, ['payments']) @@ -300,7 +303,12 @@ partiallyUpdateWorkPeriod.schema = Joi.object().keys({ currentUser: Joi.object().required(), id: Joi.string().uuid().required(), data: Joi.object().keys({ - daysWorked: Joi.number().integer().min(0).max(5) + daysWorked: Joi.number().integer().min(0).max(5), + sentSurvey: Joi.boolean(), + sentSurveyError: Joi.object().keys({ + errorCode: Joi.number().integer().min(0), + errorMessage: Joi.string() + }) }).required().min(1) }).required() @@ -499,6 +507,11 @@ searchWorkPeriods.schema = Joi.object().keys({ userHandle: Joi.string(), projectId: Joi.number().integer(), resourceBookingId: Joi.string().uuid(), + sentSurvey: Joi.boolean(), + sentSurveyError: Joi.object().keys({ + errorCode: Joi.number().integer().min(0), + errorMessage: Joi.string() + }), resourceBookingIds: Joi.alternatives( Joi.string(), Joi.array().items(Joi.string().uuid()) From cb194d63722be0d8b6e1b040172c3236cfeadcd0 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 28 Jul 2021 17:17:14 +0300 Subject: [PATCH 03/50] update: allow up to 10 daysWorked --- ...coder-bookings-api.postman_collection.json | 54 ++----------------- docs/swagger.yaml | 34 +++++++++--- .../ResourceBookingEventHandler.js | 12 +++-- src/services/ResourceBookingService.js | 2 + src/services/WorkPeriodService.js | 4 +- 5 files changed, 43 insertions(+), 63 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 8ee3ef14..bfcdc864 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "0bd597ba-4bc2-4ea1-be33-45776b80c1ce", + "_postman_id": "e9b3dfd2-60ad-45b3-9c41-87507820461d", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -15486,7 +15486,7 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"\\\"data.daysWorked\\\" must be less than or equal to 5\")\r", + " pm.expect(response.message).to.eq(\"\\\"data.daysWorked\\\" must be less than or equal to 10\")\r", "});" ], "type": "text/javascript" @@ -15504,55 +15504,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"daysWorked\": 6\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId-1}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId-1}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period with invalid parameter 4", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 400', function () {\r", - " pm.response.to.have.status(400);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"Maximum allowed daysWorked is (4)\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"daysWorked\": 5\r\n}", + "raw": "{\r\n \"daysWorked\": 11\r\n}", "options": { "raw": { "language": "json" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 81e140b7..55f22d0c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3947,7 +3947,7 @@ components: "job-closed", "offered", "withdrawn", - "withdrawn-prescreen" + "withdrawn-prescreen", ] description: "The job candidate status." externalId: @@ -4049,7 +4049,26 @@ components: type: array items: type: string - enum: ["open", "placed", "selected", "client rejected - screening", "client rejected - interview", "rejected - other", "cancelled", "interview", "topcoder-rejected", "applied", "rejected-pre-screen", "skills-test", "phone-screen", "job-closed", "offered", "withdrawn", "withdrawn-prescreen"] + enum: + [ + "open", + "placed", + "selected", + "client rejected - screening", + "client rejected - interview", + "rejected - other", + "cancelled", + "interview", + "topcoder-rejected", + "applied", + "rejected-pre-screen", + "skills-test", + "phone-screen", + "job-closed", + "offered", + "withdrawn", + "withdrawn-prescreen", + ] description: "The array of job Candidates status" JobCandidateRequestBody: required: @@ -4085,7 +4104,7 @@ components: "job-closed", "offered", "withdrawn", - "withdrawn-prescreen" + "withdrawn-prescreen", ] description: "The job candidate status." default: open @@ -4123,7 +4142,7 @@ components: "job-closed", "offered", "withdrawn", - "withdrawn-prescreen" + "withdrawn-prescreen", ] externalId: type: string @@ -4644,7 +4663,7 @@ components: daysWorked: type: integer minimum: 0 - maximum: 5 + maximum: 10 example: 2 description: "The count of the days worked for that work period." daysPaid: @@ -4696,6 +4715,8 @@ components: properties: daysWorked: type: integer + minimum: 0 + maximum: 10 example: 2 description: "The count of the days worked for that work period." WorkPeriodPayment: @@ -5275,7 +5296,7 @@ components: "job-closed", "offered", "withdrawn", - "withdrawn-prescreen" + "withdrawn-prescreen", ] description: "The job candidate status." skills: @@ -5872,4 +5893,3 @@ components: properties: message: type: string - diff --git a/src/eventHandlers/ResourceBookingEventHandler.js b/src/eventHandlers/ResourceBookingEventHandler.js index 1134674a..5ef0914e 100644 --- a/src/eventHandlers/ResourceBookingEventHandler.js +++ b/src/eventHandlers/ResourceBookingEventHandler.js @@ -188,11 +188,13 @@ async function updateWorkPeriods (payload) { const originalFirstWeek = _.find(workPeriods, ['startDate', firstWeek.startDate]) const existentFirstWeek = _.minBy(workPeriods, 'startDate') // recalculate daysWorked for the first week of existent workPeriods and daysWorked have changed - if (firstWeek.startDate === existentFirstWeek.startDate && firstWeek.daysWorked !== existentFirstWeek.daysWorked) { + if (firstWeek.startDate === existentFirstWeek.startDate && firstWeek.daysWorked !== existentFirstWeek.daysWorked && + existentFirstWeek.daysPaid <= firstWeek.daysWorked) { workPeriodsToUpdate.push(_.assign(firstWeek, { id: originalFirstWeek.id })) // if first of intersected workPeriods is not the first one of existent workPeriods // we only check if it's daysWorked exceeds the possible maximum - } else if (originalFirstWeek.daysWorked > firstWeek.daysWorked) { + } else if (originalFirstWeek.daysWorked > firstWeek.daysWorked && + originalFirstWeek.daysPaid <= firstWeek.daysWorked) { workPeriodsToUpdate.push(_.assign(firstWeek, { id: originalFirstWeek.id })) } } @@ -201,11 +203,13 @@ async function updateWorkPeriods (payload) { const originalLastWeek = _.find(workPeriods, ['startDate', lastWeek.startDate]) const existentLastWeek = _.maxBy(workPeriods, 'startDate') // recalculate daysWorked for the last week of existent workPeriods and daysWorked have changed - if (lastWeek.startDate === existentLastWeek.startDate && lastWeek.daysWorked !== existentLastWeek.daysWorked) { + if (lastWeek.startDate === existentLastWeek.startDate && lastWeek.daysWorked !== existentLastWeek.daysWorked && + existentLastWeek.daysPaid <= lastWeek.daysWorked) { workPeriodsToUpdate.push(_.assign(lastWeek, { id: originalLastWeek.id })) // if last of intersected workPeriods is not the last one of existent workPeriods // we only check if it's daysWorked exceeds the possible maximum - } else if (originalLastWeek.daysWorked > lastWeek.daysWorked) { + } else if (originalLastWeek.daysWorked > lastWeek.daysWorked && + originalLastWeek.daysPaid <= lastWeek.daysWorked) { workPeriodsToUpdate.push(_.assign(lastWeek, { id: originalLastWeek.id })) } } diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 46d2fe62..ba7475dd 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -251,6 +251,7 @@ async function _ensurePaidWorkPeriodsNotDeleted (resourceBookingId, oldValue, ne // or any of it's WorkPeriodsPayment has status 'completed' or 'in-progress'. _checkForPaidWorkPeriods(workPeriodsToRemove) // check if this update makes maximum possible daysWorked value less than daysPaid + /* https://github.com/topcoder-platform/taas-apis/issues/428 _.each(newWorkPeriods, newWP => { const wp = _.find(workPeriods, ['startDate', newWP.startDate]) if (!wp) { @@ -260,6 +261,7 @@ async function _ensurePaidWorkPeriodsNotDeleted (resourceBookingId, oldValue, ne throw new errors.ConflictError(`Cannot make maximum daysWorked (${newWP.daysWorked}) to the value less than daysPaid (${wp.daysPaid}) for WorkPeriod: ${wp.id}`) } }) + */ } /** diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index 8d018fb0..702161a5 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -273,9 +273,11 @@ async function updateWorkPeriod (currentUser, id, data) { if (_.isNil(thisWeek)) { throw new errors.ConflictError('Work Period dates are not compatible with Resource Booking dates') } + /* https://github.com/topcoder-platform/taas-apis/issues/428 if (thisWeek.daysWorked < data.daysWorked) { throw new errors.BadRequestError(`Maximum allowed daysWorked is (${thisWeek.daysWorked})`) } + */ data.paymentStatus = helper.calculateWorkPeriodPaymentStatus(_.assign({}, oldValue, data)) data.updatedBy = await helper.getUserId(currentUser.userId) const updated = await workPeriod.update(data) @@ -300,7 +302,7 @@ partiallyUpdateWorkPeriod.schema = Joi.object().keys({ currentUser: Joi.object().required(), id: Joi.string().uuid().required(), data: Joi.object().keys({ - daysWorked: Joi.number().integer().min(0).max(5) + daysWorked: Joi.number().integer().min(0).max(10) }).required().min(1) }).required() From 99f97b360eb06f266ee4f65d4c7b4ec458bb4f38 Mon Sep 17 00:00:00 2001 From: narekcat Date: Wed, 28 Jul 2021 18:27:02 +0400 Subject: [PATCH 04/50] fix: additional payments --- src/services/WorkPeriodPaymentService.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index 9a7a7780..82e82934 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -80,7 +80,7 @@ async function _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking (w throw new errors.BadRequestError(`Cannot process payments for the future WorkPeriods. You can process after ${workPeriodStartTime.diff(moment(), 'hours')} hours`) } workPeriodPayment.days = _.defaultTo(workPeriodPayment.days, maxPossibleDays) - workPeriodPayment.amount = _.round(workPeriodPayment.memberRate * workPeriodPayment.days / 5, 2) + workPeriodPayment.amount = workPeriodPayment.days > 0 ? _.round(workPeriodPayment.memberRate * workPeriodPayment.days / 5, 2) : workPeriodPayment.amount workPeriodPayment.customerRate = _.defaultTo(correspondingResourceBooking.customerRate, null) workPeriodPayment.id = uuid.v4() workPeriodPayment.status = WorkPeriodPaymentStatus.SCHEDULED @@ -185,7 +185,8 @@ async function createWorkPeriodPayment (currentUser, workPeriodPayment) { const singleCreateWorkPeriodPaymentSchema = Joi.object().keys({ workPeriodId: Joi.string().uuid().required(), - days: Joi.number().integer().min(1).max(5) + days: Joi.number().integer().min(0).max(5), + amount: Joi.number() }) createWorkPeriodPayment.schema = Joi.object().keys({ currentUser: Joi.object().required(), From 0c7e49876a620945baff2abc17613cb6bb9dd031 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 29 Jul 2021 00:24:14 +0300 Subject: [PATCH 05/50] update: search up to 10 days --- docs/Topcoder-bookings-api.postman_collection.json | 6 +++--- src/services/ResourceBookingService.js | 2 +- src/services/WorkPeriodPaymentService.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index bfcdc864..f675ea17 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "e9b3dfd2-60ad-45b3-9c41-87507820461d", + "_postman_id": "8ead1433-9679-46de-9baa-d27d59106673", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -16460,7 +16460,7 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.days\\\" must be less than or equal to 5\")\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.days\\\" must be less than or equal to 10\")\r", "});" ], "type": "text/javascript" @@ -16478,7 +16478,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId-3}}\",\r\n \"days\": 6\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId-3}}\",\r\n \"days\": 11\r\n}", "options": { "raw": { "language": "json" diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index ba7475dd..294f4c2d 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -908,7 +908,7 @@ searchResourceBookings.schema = Joi.object().keys({ }) }), 'workPeriods.payments.status': Joi.workPeriodPaymentStatus(), - 'workPeriods.payments.days': Joi.number().integer().min(0).max(5) + 'workPeriods.payments.days': Joi.number().integer().min(0).max(10) }).required(), options: Joi.object().keys({ returnAll: Joi.boolean().default(false), diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index 9a7a7780..7c88d1b8 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -185,7 +185,7 @@ async function createWorkPeriodPayment (currentUser, workPeriodPayment) { const singleCreateWorkPeriodPaymentSchema = Joi.object().keys({ workPeriodId: Joi.string().uuid().required(), - days: Joi.number().integer().min(1).max(5) + days: Joi.number().integer().min(1).max(10) }) createWorkPeriodPayment.schema = Joi.object().keys({ currentUser: Joi.object().required(), From 66a2ec285b33a5e02bff42c21be69c80f7d13beb Mon Sep 17 00:00:00 2001 From: yoution Date: Thu, 29 Jul 2021 07:45:13 +0800 Subject: [PATCH 06/50] fix: Edit payments fields #431 --- ...coder-bookings-api.postman_collection.json | 14 ++--- docs/swagger.yaml | 25 ++++++++ src/services/WorkPeriodPaymentService.js | 60 ++++++++++++++++++- 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 8ee3ef14..c7fab8b9 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "0bd597ba-4bc2-4ea1-be33-45776b80c1ce", + "_postman_id": "9d8bfd2b-8864-49fd-8c6e-4cff10095cc3", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -17279,7 +17279,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"status\": \"cancelled\"\r\n}", + "raw": "{\r\n \"status\": \"cancelled\",\r\n \"days\":1,\r\n \"amount\":2,\r\n \"memberRate\":1,\r\n \"customerRate\":3,\r\n \"billingAccountId\": 23\r\n}", "options": { "raw": { "language": "json" @@ -17325,7 +17325,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"status\": \"cancelled\"\r\n}", + "raw": "{\r\n \"status\": \"cancelled\",\r\n \"days\":1,\r\n \"amount\":2,\r\n \"memberRate\":1,\r\n \"customerRate\":3,\r\n \"billingAccountId\": 23\r\n}", "options": { "raw": { "language": "json" @@ -17373,7 +17373,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"status\": \"cancelled\"\r\n}", + "raw": "{\r\n \"status\": \"cancelled\",\r\n \"days\":1,\r\n \"amount\":2,\r\n \"memberRate\":1,\r\n \"customerRate\":3,\r\n \"billingAccountId\": 23\r\n}", "options": { "raw": { "language": "json" @@ -17421,7 +17421,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"status\": \"cancelled\"\r\n}", + "raw": "{\r\n \"status\": \"cancelled\",\r\n \"days\":1,\r\n \"amount\":2,\r\n \"memberRate\":1,\r\n \"customerRate\":3,\r\n \"billingAccountId\": 23\r\n}", "options": { "raw": { "language": "json" @@ -17469,7 +17469,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"status\": \"cancelled\"\r\n}", + "raw": "{\r\n \"status\": \"cancelled\",\r\n \"days\":1,\r\n \"amount\":2,\r\n \"memberRate\":1,\r\n \"customerRate\":3,\r\n \"billingAccountId\": 23\r\n}", "options": { "raw": { "language": "json" @@ -17517,7 +17517,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"status\": \"cancelled\"\r\n}", + "raw": "{\r\n \"status\": \"cancelled\",\r\n \"days\":1,\r\n \"amount\":2,\r\n \"memberRate\":1,\r\n \"customerRate\":3,\r\n \"billingAccountId\": 23\r\n}", "options": { "raw": { "language": "json" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 81e140b7..fee7280c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -4887,6 +4887,30 @@ components: type: string enum: ["scheduled", "cancelled"] description: "The payment status." + memberRate: + type: integer + format: float + example: 13 + description: "The member rate." + customerRate: + type: integer + format: float + example: 13 + description: "The customer rate." + billingAccountId: + type: integer + example: 80000071 + description: "the billing account id for payments" + days: + type: integer + minimum: 0 + example: 3 + description: "The workdays to pay" + amount: + type: integer + format: float + example: 2 + description: "The amount to be paid." CheckRun: type: object properties: @@ -5873,3 +5897,4 @@ components: message: type: string + diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index 9a7a7780..852e8cc4 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -48,6 +48,38 @@ async function _createSingleWorkPeriodPayment (workPeriodPayment, createdBy) { return _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking(workPeriodPayment, createdBy, correspondingWorkPeriod.toJSON(), correspondingResourceBooking.toJSON()) } +/** + * Create single workPeriodPayment + * @param {Object} workPeriodPayment the workPeriodPayment to be created + * @param {String} createdBy the authUser id + * @returns {Object} the created workPeriodPayment + */ +async function _updateChallenge (challengeId, data) { + const body = {} + if (data.billingAccountId) { + body.billing = { + billingAccountId: _.toString(data.billingAccountId), + markup: 0 // for TaaS payments we always use 0 markup + } + } + if (data.amount) { + body.prizeSets = [{ + type: 'placement', + prizes: [{ type: 'USD', value: data.amount }] + }] + } + + if (data.billingAccountId || data.amount) { + try { + await helper.updateChallenge(challengeId, body) + logger.debug({ component: 'WorkPeriodPaymentService', context: 'updateChallenge', message: `Challenge with id ${challengeId} is updated` }) + } catch (err) { + logger.error({ component: 'WorkPeriodPaymentService', context: 'updateChallenge', message: err.response.text }) + throw new errors.BadRequestError(`Cannot update the the challenge: ${err.response.text}`) + } + } +} + /** * Create single workPeriodPayment * @param {Object} workPeriodPayment the workPeriodPayment to be created @@ -207,7 +239,15 @@ async function updateWorkPeriodPayment (currentUser, id, data) { _checkUserPermissionForCRUWorkPeriodPayment(currentUser) const workPeriodPayment = await WorkPeriodPayment.findById(id) + const oldValue = workPeriodPayment.toJSON() + + if (oldValue.status === 'in-progress') { + _.each(_.pick(data, ['amount', 'days', 'memberRate', 'customerRate', 'billingAccountId']), (value, key) => { + throw new errors.BadRequestError(`${key} cannot be updated when workPeriodPayment status is in-progress`) + }) + } + if (data.status === 'cancelled' && oldValue.status === 'in-progress') { throw new errors.BadRequestError('You cannot cancel a WorkPeriodPayment which is in-progress') } @@ -222,6 +262,19 @@ async function updateWorkPeriodPayment (currentUser, id, data) { throw new errors.BadRequestError('There is no available daysWorked to schedule a payment') } } + + if (data.days) { + const correspondingWorkPeriod = await helper.ensureWorkPeriodById(workPeriodPayment.workPeriodId) // ensure work period exists + const maxPossibleDays = correspondingWorkPeriod.daysWorked - correspondingWorkPeriod.daysPaid + if (data.days > maxPossibleDays) { + throw new errors.BadRequestError(`Days cannot be more than not paid days which is ${maxPossibleDays}`) + } + } + + if (oldValue.challengeId) { + await _updateChallenge(workPeriodPayment.challengeId, data) + } + data.updatedBy = await helper.getUserId(currentUser.userId) const updated = await workPeriodPayment.update(data) await helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue, key: `workPeriodPayment.billingAccountId:${updated.billingAccountId}` }) @@ -243,7 +296,12 @@ partiallyUpdateWorkPeriodPayment.schema = Joi.object().keys({ currentUser: Joi.object().required(), id: Joi.string().uuid().required(), data: Joi.object().keys({ - status: Joi.workPeriodPaymentUpdateStatus() + status: Joi.workPeriodPaymentUpdateStatus(), + amount: Joi.number().min(0), + days: Joi.number().integer(), + memberRate: Joi.number().positive().required(), + customerRate: Joi.number().positive().allow(null), + billingAccountId: Joi.number().positive().integer().required() }).min(1).required() }).required() From 60d476e0efff030bcc2b8844a36415252b576da6 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 29 Jul 2021 17:06:56 +0300 Subject: [PATCH 07/50] fix: script to import job from RCRM --- scripts/recruit-crm-job-import/README.md | 27 ++++++---- scripts/recruit-crm-job-import/helper.js | 8 +-- scripts/recruit-crm-job-import/index.js | 53 +++++++++++++------ scripts/recruit-crm-job-import/report.js | 10 ++++ .../ResourceBookingEventHandler.js | 4 +- 5 files changed, 68 insertions(+), 34 deletions(-) diff --git a/scripts/recruit-crm-job-import/README.md b/scripts/recruit-crm-job-import/README.md index fdd8e01a..5653794c 100644 --- a/scripts/recruit-crm-job-import/README.md +++ b/scripts/recruit-crm-job-import/README.md @@ -16,6 +16,7 @@ By default the script creates jobs and resource bookings via `TC_API`. Follow the README for Taas API to deploy Taas API locally and then point the script to the local API by running: ``` bash +export RCRM_IMPORT_CONFIG_NAMESAPCE=RCRM_IMPORT_ export RCRM_IMPORT_TAAS_API_URL=http://localhost:3000/api/v5 node scripts/recruit-crm-job-import scripts/recruit-crm-job-import/example_data.csv | tee /tmp/report.txt ``` @@ -27,38 +28,42 @@ DEBUG: processing line #1 - {"directProjectId":"24568","projectId":"(dynamic loa WARN: #1 - externalId is missing DEBUG: processed line #1 DEBUG: processing line #2 - {"directProjectId":"24568","projectId":"(dynamic load)","externalId":"0","title":"taas-demo-job5","startDate":"10/26/2020","endDate":"01/29/2021","numPositions":"2","userHandle":"not_found_handle","jobid":"(dynamic load)","customerRate":"20","memberRate":"10","_lnum":2} -ERROR: #2 - handle: not_found_handle user not found +ERROR: #2 - id: 51ce2216-0dee-4dcf-bf7d-79f862e8d63c job created; handle: not_found_handle user not found DEBUG: processed line #2 DEBUG: processing line #3 - {"directProjectId":"24568","projectId":"(dynamic load)","externalId":"0","title":"taas-demo-job5","startDate":"10/26/2020","endDate":"01/29/2021","numPositions":"2","userHandle":"nkumartest","jobid":"(dynamic load)","customerRate":"20","memberRate":"10","_lnum":3} DEBUG: userHandle: nkumartest userId: 57646ff9-1cd3-4d3c-88ba-eb09a395366c -DEBUG: resourceBookingId: dc8b23d4-9987-4a7d-a587-2056283223de status: assigned -INFO: #3 - id: 7c8ed989-35bf-4899-9c93-708630a7c63b job already exists; id: dc8b23d4-9987-4a7d-a587-2056283223de resource booking created; id: dc8b23d4-9987-4a7d-a587-2056283223de status: assigned resource booking updated +INFO: #3 - id: 51ce2216-0dee-4dcf-bf7d-79f862e8d63c externalId: 0 job already exists; id: d49d2fbd-ba11-49dc-8eaa-5afafa7e993f resource booking created DEBUG: processed line #3 DEBUG: processing line #4 - {"directProjectId":"24567","projectId":"(dynamic load)","externalId":"1212","title":"Dummy Description","startDate":"10/20/2020","endDate":"01/29/2021","numPositions":"2","userHandle":"pshah_manager","jobid":"(dynamic load)","customerRate":"150","memberRate":"100","_lnum":4} DEBUG: userHandle: pshah_manager userId: a55fe1bc-1754-45fa-9adc-cf3d6d7c377a -DEBUG: resourceBookingId: 708469fb-ead0-4fc3-bef7-1ef4dd041428 status: assigned -INFO: #4 - id: f61da880-5295-40c2-b6db-21e6cdef93f9 job created; id: 708469fb-ead0-4fc3-bef7-1ef4dd041428 resource booking created; id: 708469fb-ead0-4fc3-bef7-1ef4dd041428 status: assigned resource booking updated +INFO: #4 - id: e0267551-24fe-48b5-9605-719852901de2 job created; id: f6285f03-056d-446f-a69b-6d275a97d68a resource booking created DEBUG: processed line #4 DEBUG: processing line #5 - {"directProjectId":"24566","projectId":"(dynamic load)","externalId":"23850272","title":"33fromzaps330","startDate":"02/21/2021","endDate":"03/15/2021","numPositions":"7","userHandle":"nkumar2","jobid":"(dynamic load)","customerRate":"50","memberRate":"30","_lnum":5} DEBUG: userHandle: nkumar2 userId: 4b00d029-c87b-47b2-bfe2-0ab80d8b5774 -DEBUG: resourceBookingId: 7870c30b-e511-48f2-8687-499ab116174f status: assigned -INFO: #5 - id: 72dc0399-5e4b-4783-9a27-ea07a4ce99a7 job created; id: 7870c30b-e511-48f2-8687-499ab116174f resource booking created; id: 7870c30b-e511-48f2-8687-499ab116174f status: assigned resource booking updated +INFO: #5 - id: cd94784c-432d-4c46-b860-04a89e7b1099 job created; id: 98604c13-c6f3-4203-b74f-db376e9f02e4 resource booking created DEBUG: processed line #5 DEBUG: processing line #6 - {"directProjectId":"24565","projectId":"(dynamic load)","externalId":"23843365","title":"Designer","startDate":"02/24/2021","endDate":"03/30/2021","numPositions":"1","userHandle":"GunaK-TopCoder","jobid":"(dynamic load)","customerRate":"70","memberRate":"70","_lnum":6} DEBUG: userHandle: GunaK-TopCoder userId: 2bba34d5-20e4-46d6-bfc1-05736b17afbb -DEBUG: resourceBookingId: b2e705d3-6864-4697-96bb-dc2a288755bc status: assigned -INFO: #6 - id: 7ff0737e-958c-494e-8a5a-592ac1c5d4ff job created; id: b2e705d3-6864-4697-96bb-dc2a288755bc resource booking created; id: b2e705d3-6864-4697-96bb-dc2a288755bc status: assigned resource booking updated +INFO: #6 - id: 49883150-59c2-4e5b-b5c3-aaf6d11d0da2 job created; id: 5505b6b5-050c-421c-893f-b862b1a08092 resource booking created DEBUG: processed line #6 DEBUG: processing line #7 - {"directProjectId":"24564","projectId":"(dynamic load)","externalId":"23836459","title":"demo-dev-19janV4","startDate":"01/20/2021","endDate":"01/30/2021","numPositions":"1","userHandle":"nkumar1","jobid":"(dynamic load)","customerRate":"400","memberRate":"200","_lnum":7} DEBUG: userHandle: nkumar1 userId: ab19a53b-0607-4a99-8bdd-f3b0cb552293 -DEBUG: resourceBookingId: 04299b4c-3f6e-4b3e-ae57-bf8232408cf9 status: assigned -INFO: #7 - id: 73301ade-40ff-4103-bd50-37b8d2a98183 job created; id: 04299b4c-3f6e-4b3e-ae57-bf8232408cf9 resource booking created; id: 04299b4c-3f6e-4b3e-ae57-bf8232408cf9 status: assigned resource booking updated +INFO: #7 - id: b03dc641-d6be-4a15-9c86-ef38f0e20c28 job created; id: 8e332107-453b-4ec5-b934-902c829e73a2 resource booking created DEBUG: processed line #7 INFO: === summary === INFO: total: 7 INFO: success: 5 INFO: failure: 1 INFO: skips: 1 +INFO: jobs created: 5 +INFO: resource bookings created: 5 +INFO: jobs already exist: 1 +INFO: resource bookings already exist: 0 +INFO: validation errors: 0 +INFO: user not found: 1 +INFO: external id missing: 1 +INFO: request error: 0 +INFO: internal error: 0 INFO: === summary === INFO: done! ``` diff --git a/scripts/recruit-crm-job-import/helper.js b/scripts/recruit-crm-job-import/helper.js index 43591ff3..b3096866 100644 --- a/scripts/recruit-crm-job-import/helper.js +++ b/scripts/recruit-crm-job-import/helper.js @@ -54,15 +54,15 @@ async function getJobByExternalId (externalId) { * Update the status of a resource booking. * * @param {String} resourceBookingId the resource booking id - * @param {String} status the status for the resource booking + * @param {Object} data the data to update * @returns {Object} the result */ -async function updateResourceBookingStatus (resourceBookingId, status) { +async function updateResourceBooking (resourceBookingId, data) { const token = await getM2MToken() const { body: resourceBooking } = await request.patch(`${config.TAAS_API_URL}/resourceBookings/${resourceBookingId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .send({ status }) + .send(data) return resourceBooking } @@ -139,7 +139,7 @@ module.exports = { getPathnameFromCommandline: commonHelper.getPathnameFromCommandline, createJob, getJobByExternalId, - updateResourceBookingStatus, + updateResourceBooking, getResourceBookingByJobIdAndUserId, createResourceBooking, getUserByHandle, diff --git a/scripts/recruit-crm-job-import/index.js b/scripts/recruit-crm-job-import/index.js index 596ab680..100ad315 100644 --- a/scripts/recruit-crm-job-import/index.js +++ b/scripts/recruit-crm-job-import/index.js @@ -5,7 +5,7 @@ const Joi = require('joi') .extend(require('@joi/date')) const _ = require('lodash') -const dateFNS = require('date-fns') +const moment = require('moment') const Report = require('./report') const config = require('./config') const helper = require('./helper') @@ -18,12 +18,12 @@ const jobSchema = Joi.object({ title: Joi.string().required(), startDate: Joi.date().format('MM/DD/YYYY').required(), endDate: Joi.date().format('MM/DD/YYYY').required(), - numPositions: Joi.number().integer().min(1), + numPositions: Joi.number().integer().min(1).required(), userHandle: Joi.string(), customerRate: Joi.number(), memberRate: Joi.number(), skills: Joi.array().default([]), - rateType: Joi.string().default('weekly') + rateType: Joi.string().default('weekly').valid('hourly', 'daily', 'weekly', 'monthly', 'annual') }).unknown(true) /** @@ -67,14 +67,28 @@ async function processJob (job, info = []) { data.jobId = result.id } catch (err) { if (!(err.message && err.message.includes('job not found'))) { + err.info = info throw err } - const result = await helper.createJob(_.pick(data, ['projectId', 'externalId', 'title', 'numPositions', 'skills'])) + const jobData = _.pick(data, ['projectId', 'externalId', 'title', 'numPositions', 'skills']) + if (data.numPositions === 1) { + jobData.status = 'assigned' + } + const result = await helper.createJob(jobData) info.push({ text: `id: ${result.id} job created`, tag: 'job_created' }) data.jobId = result.id } - data.userId = (await helper.getUserByHandle(data.userHandle)).id - logger.debug(`userHandle: ${data.userHandle} userId: ${data.userId}`) + try { + data.userId = (await helper.getUserByHandle(data.userHandle)).id + logger.debug(`userHandle: ${data.userHandle} userId: ${data.userId}`) + } catch (err) { + if (!(err.message && err.message.includes('user not found'))) { + err.info = info + throw err + } + info.push({ text: err.message, tag: 'user_not_found' }) + return { status: constants.ProcessingStatus.Failed, info } + } // create a resource booking if it does not already exist try { const result = await helper.getResourceBookingByJobIdAndUserId(data.jobId, data.userId) @@ -82,18 +96,22 @@ async function processJob (job, info = []) { return { status: constants.ProcessingStatus.Successful, info } } catch (err) { if (!(err.message && err.message.includes('resource booking not found'))) { + err.info = info + throw err + } + try { + const resourceBookingData = _.pick(data, ['projectId', 'jobId', 'userId', 'memberRate', 'customerRate', 'rateType']) + resourceBookingData.startDate = moment(data.startDate).format('YYYY-MM-DD') + resourceBookingData.endDate = moment(data.endDate).format('YYYY-MM-DD') + resourceBookingData.status = moment(data.endDate).isBefore(moment()) ? 'closed' : 'placed' + const result = await helper.createResourceBooking(resourceBookingData) + info.push({ text: `id: ${result.id} resource booking created`, tag: 'resource_booking_created' }) + return { status: constants.ProcessingStatus.Successful, info } + } catch (err) { + err.info = info throw err } - const result = await helper.createResourceBooking(_.pick(data, ['projectId', 'jobId', 'userId', 'startDate', 'endDate', 'memberRate', 'customerRate', 'rateType'])) - info.push({ text: `id: ${result.id} resource booking created`, tag: 'resource_booking_created' }) - data.resourceBookingId = result.id } - // update the resourceBooking based on startDate and endDate - const resourceBookingStatus = dateFNS.isBefore(data.endDate, dateFNS.startOfToday()) ? 'closed' : 'placed' - logger.debug(`resourceBookingId: ${data.resourceBookingId} status: ${resourceBookingStatus}`) - await helper.updateResourceBookingStatus(data.resourceBookingId, resourceBookingStatus) - info.push({ text: `id: ${data.resourceBookingId} status: ${resourceBookingStatus} resource booking updated`, tag: 'resource_booking_status_updated' }) - return { status: constants.ProcessingStatus.Successful, info } } /** @@ -111,10 +129,11 @@ async function main () { const result = await processJob(job) report.add({ lnum: job._lnum, ...result }) } catch (err) { + const info = err.info || [] if (err.response) { - report.add({ lnum: job._lnum, status: constants.ProcessingStatus.Failed, info: [{ text: err.response.error.toString().split('\n')[0], tag: 'request_error' }] }) + report.add({ lnum: job._lnum, status: constants.ProcessingStatus.Failed, info: [{ text: err.response.error.toString().split('\n')[0], tag: 'request_error' }, ...info] }) } else { - report.add({ lnum: job._lnum, status: constants.ProcessingStatus.Failed, info: [{ text: err.message, tag: 'internal_error' }] }) + report.add({ lnum: job._lnum, status: constants.ProcessingStatus.Failed, info: [{ text: err.message, tag: 'internal_error' }, ...info] }) } } report.print() diff --git a/scripts/recruit-crm-job-import/report.js b/scripts/recruit-crm-job-import/report.js index ef31a4c8..b1b53bb2 100644 --- a/scripts/recruit-crm-job-import/report.js +++ b/scripts/recruit-crm-job-import/report.js @@ -44,6 +44,11 @@ class Report { const resourceBookingsCreated = groupsByTag.resource_booking_created || [] const jobsAlreadyExist = groupsByTag.job_already_exists || [] const resourceBookingsAlreadyExist = groupsByTag.resource_booking_already_exists || [] + const validationErrors = groupsByTag.validation_error || [] + const userNotFound = groupsByTag.user_not_found || [] + const externalIdMissing = groupsByTag.external_id_missing || [] + const requestError = groupsByTag.request_error || [] + const internalError = groupsByTag.internal_error || [] logger.info('=== summary ===') logger.info(`total: ${this.messages.length}`) logger.info(`success: ${success.length}`) @@ -53,6 +58,11 @@ class Report { logger.info(`resource bookings created: ${resourceBookingsCreated.length}`) logger.info(`jobs already exist: ${jobsAlreadyExist.length}`) logger.info(`resource bookings already exist: ${resourceBookingsAlreadyExist.length}`) + logger.info(`validation errors: ${validationErrors.length}`) + logger.info(`user not found: ${userNotFound.length}`) + logger.info(`external id missing: ${externalIdMissing.length}`) + logger.info(`request error: ${requestError.length}`) + logger.info(`internal error: ${internalError.length}`) logger.info('=== summary ===') } } diff --git a/src/eventHandlers/ResourceBookingEventHandler.js b/src/eventHandlers/ResourceBookingEventHandler.js index 1134674a..1743d580 100644 --- a/src/eventHandlers/ResourceBookingEventHandler.js +++ b/src/eventHandlers/ResourceBookingEventHandler.js @@ -102,11 +102,11 @@ async function assignJob (payload) { return } const job = await models.Job.findById(resourceBooking.jobId) - if (job.status === 'placed') { + if (job.status === 'placed' || job.status === 'assigned') { logger.debug({ component: 'ResourceBookingEventHandler', context: 'assignJob', - message: `job with projectId ${job.projectId} is already placed` + message: `job with projectId ${job.projectId} is already ${job.status}` }) return } From 3fe30ad9bc6036df96b5bba07fbb51c8c629ae3b Mon Sep 17 00:00:00 2001 From: narekcat Date: Thu, 29 Jul 2021 18:28:51 +0400 Subject: [PATCH 08/50] fix: additional payments, add joi validation rule for amount field --- src/services/WorkPeriodPaymentService.js | 49 ++++++++++++++---------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index 82e82934..5e0f6a3e 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -61,26 +61,29 @@ async function _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking (w throw new errors.ConflictError(`id: ${correspondingResourceBooking.id} "ResourceBooking" Billing account is not assigned to the resource booking`) } workPeriodPayment.billingAccountId = correspondingResourceBooking.billingAccountId - if (_.isNil(correspondingResourceBooking.memberRate)) { - throw new errors.ConflictError(`Can't find a member rate in ResourceBooking: ${correspondingResourceBooking.id} to calculate the amount`) - } - if (correspondingResourceBooking.memberRate <= 0) { - throw new errors.ConflictError(`Can't process payment with member rate: ${correspondingResourceBooking.memberRate}. It must be higher than 0`) - } - workPeriodPayment.memberRate = correspondingResourceBooking.memberRate - const maxPossibleDays = correspondingWorkPeriod.daysWorked - correspondingWorkPeriod.daysPaid - if (workPeriodPayment.days > maxPossibleDays) { - throw new errors.BadRequestError(`Days cannot be more than not paid days which is ${maxPossibleDays}`) - } - if (maxPossibleDays <= 0) { - throw new errors.ConflictError(`There are no days to pay for WorkPeriod: ${correspondingWorkPeriod.id}`) - } - const workPeriodStartTime = moment(`${correspondingWorkPeriod.startDate}T00:00:00.000+12`) - if (workPeriodStartTime.isAfter(moment())) { - throw new errors.BadRequestError(`Cannot process payments for the future WorkPeriods. You can process after ${workPeriodStartTime.diff(moment(), 'hours')} hours`) + if (!_.has(workPeriodPayment, 'days') || workPeriodPayment.days > 0) { + if (_.isNil(correspondingResourceBooking.memberRate)) { + throw new errors.ConflictError(`Can't find a member rate in ResourceBooking: ${correspondingResourceBooking.id} to calculate the amount`) + } + if (correspondingResourceBooking.memberRate <= 0) { + throw new errors.ConflictError(`Can't process payment with member rate: ${correspondingResourceBooking.memberRate}. It must be higher than 0`) + } + workPeriodPayment.memberRate = correspondingResourceBooking.memberRate + const maxPossibleDays = correspondingWorkPeriod.daysWorked - correspondingWorkPeriod.daysPaid + if (workPeriodPayment.days > maxPossibleDays) { + throw new errors.BadRequestError(`Days cannot be more than not paid days which is ${maxPossibleDays}`) + } + if (maxPossibleDays <= 0) { + throw new errors.ConflictError(`There are no days to pay for WorkPeriod: ${correspondingWorkPeriod.id}`) + } + const workPeriodStartTime = moment(`${correspondingWorkPeriod.startDate}T00:00:00.000+12`) + if (workPeriodStartTime.isAfter(moment())) { + throw new errors.BadRequestError(`Cannot process payments for the future WorkPeriods. You can process after ${workPeriodStartTime.diff(moment(), 'hours')} hours`) + } + workPeriodPayment.days = _.defaultTo(workPeriodPayment.days, maxPossibleDays) + workPeriodPayment.amount = _.round(workPeriodPayment.memberRate * workPeriodPayment.days / 5, 2) } - workPeriodPayment.days = _.defaultTo(workPeriodPayment.days, maxPossibleDays) - workPeriodPayment.amount = workPeriodPayment.days > 0 ? _.round(workPeriodPayment.memberRate * workPeriodPayment.days / 5, 2) : workPeriodPayment.amount + workPeriodPayment.memberRate = _.defaultTo(workPeriodPayment.memberRate, 0) workPeriodPayment.customerRate = _.defaultTo(correspondingResourceBooking.customerRate, null) workPeriodPayment.id = uuid.v4() workPeriodPayment.status = WorkPeriodPaymentStatus.SCHEDULED @@ -186,7 +189,13 @@ async function createWorkPeriodPayment (currentUser, workPeriodPayment) { const singleCreateWorkPeriodPaymentSchema = Joi.object().keys({ workPeriodId: Joi.string().uuid().required(), days: Joi.number().integer().min(0).max(5), - amount: Joi.number() + amount: Joi.when('days', { + is: Joi.number().integer().valid(0).exist(), + then: Joi.number().greater(0).required().messages({ + 'any.required': '"amount" has to be provided when processing additional payment for 0 days' + }), + otherwise: Joi.forbidden() + }) }) createWorkPeriodPayment.schema = Joi.object().keys({ currentUser: Joi.object().required(), From 8949ec305094f640cca76545babe584fcc523a24 Mon Sep 17 00:00:00 2001 From: yoution Date: Thu, 29 Jul 2021 23:40:15 +0800 Subject: [PATCH 09/50] fix: issue #431 --- src/services/WorkPeriodPaymentService.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index 852e8cc4..08839ae8 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -49,10 +49,10 @@ async function _createSingleWorkPeriodPayment (workPeriodPayment, createdBy) { } /** - * Create single workPeriodPayment - * @param {Object} workPeriodPayment the workPeriodPayment to be created - * @param {String} createdBy the authUser id - * @returns {Object} the created workPeriodPayment + * update challenge + * @param {String} challengeId the challenge id + * @param {Object} data the challenge update data + * @returns {undefined} */ async function _updateChallenge (challengeId, data) { const body = {} @@ -243,9 +243,10 @@ async function updateWorkPeriodPayment (currentUser, id, data) { const oldValue = workPeriodPayment.toJSON() if (oldValue.status === 'in-progress') { - _.each(_.pick(data, ['amount', 'days', 'memberRate', 'customerRate', 'billingAccountId']), (value, key) => { - throw new errors.BadRequestError(`${key} cannot be updated when workPeriodPayment status is in-progress`) - }) + const keys = _.keys(_.pick(data, ['amount', 'days', 'memberRate', 'customerRate', 'billingAccountId'])) + if (keys.length) { + throw new errors.BadRequestError(`${JSON.stringify(keys)} cannot be updated when workPeriodPayment status is in-progress`) + } } if (data.status === 'cancelled' && oldValue.status === 'in-progress') { @@ -265,13 +266,14 @@ async function updateWorkPeriodPayment (currentUser, id, data) { if (data.days) { const correspondingWorkPeriod = await helper.ensureWorkPeriodById(workPeriodPayment.workPeriodId) // ensure work period exists - const maxPossibleDays = correspondingWorkPeriod.daysWorked - correspondingWorkPeriod.daysPaid + const maxPossibleDays = correspondingWorkPeriod.daysWorked - correspondingWorkPeriod.daysPaid - oldValue.days if (data.days > maxPossibleDays) { throw new errors.BadRequestError(`Days cannot be more than not paid days which is ${maxPossibleDays}`) } } - if (oldValue.challengeId) { + // challengeId exist and skip dummy challenge + if (oldValue.challengeId && oldValue.challengeId !== '00000000-0000-0000-0000-000000000000') { await _updateChallenge(workPeriodPayment.challengeId, data) } @@ -299,9 +301,9 @@ partiallyUpdateWorkPeriodPayment.schema = Joi.object().keys({ status: Joi.workPeriodPaymentUpdateStatus(), amount: Joi.number().min(0), days: Joi.number().integer(), - memberRate: Joi.number().positive().required(), + memberRate: Joi.number().positive(), customerRate: Joi.number().positive().allow(null), - billingAccountId: Joi.number().positive().integer().required() + billingAccountId: Joi.number().positive().integer() }).min(1).required() }).required() From 1afad93990dd607727f3b609c51b1b7fd329bd8a Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 30 Jul 2021 10:26:09 +0300 Subject: [PATCH 10/50] fix: memberRate for additional payments ref issue #430 --- src/services/WorkPeriodPaymentService.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index 5e0f6a3e..6d40f4cd 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -68,7 +68,7 @@ async function _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking (w if (correspondingResourceBooking.memberRate <= 0) { throw new errors.ConflictError(`Can't process payment with member rate: ${correspondingResourceBooking.memberRate}. It must be higher than 0`) } - workPeriodPayment.memberRate = correspondingResourceBooking.memberRate + const maxPossibleDays = correspondingWorkPeriod.daysWorked - correspondingWorkPeriod.daysPaid if (workPeriodPayment.days > maxPossibleDays) { throw new errors.BadRequestError(`Days cannot be more than not paid days which is ${maxPossibleDays}`) @@ -83,7 +83,8 @@ async function _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking (w workPeriodPayment.days = _.defaultTo(workPeriodPayment.days, maxPossibleDays) workPeriodPayment.amount = _.round(workPeriodPayment.memberRate * workPeriodPayment.days / 5, 2) } - workPeriodPayment.memberRate = _.defaultTo(workPeriodPayment.memberRate, 0) + // TODO: we should allow `memberRate` to be `null` as it's not required for additional payments + workPeriodPayment.memberRate = _.defaultTo(correspondingResourceBooking.memberRate, 0) workPeriodPayment.customerRate = _.defaultTo(correspondingResourceBooking.customerRate, null) workPeriodPayment.id = uuid.v4() workPeriodPayment.status = WorkPeriodPaymentStatus.SCHEDULED From 358428e208e5adec985ef45881ff2ce066bd539d Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sat, 31 Jul 2021 11:17:17 +0300 Subject: [PATCH 11/50] added NPM command to rebuild service in docker --- README.md | 5 +++-- package.json | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a61b99dd..9c32d290 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,7 @@ To be able to change and test `taas-es-processor` locally you can follow the nex | `npm run services:up` | Start services via docker-compose for local development. | | `npm run services:down` | Stop services via docker-compose for local development. | | `npm run services:logs -- -f ` | View logs of some service inside docker-compose. | +| `npm run services:rebuild -- -f ` | Rebuild service container ignoring cache (useful when pushed something to the Git repository of service) | | `npm run local:init` | Recreate Database and Elasticsearch indexes and populate demo data for local development (removes any existent data). | | `npm run local:reset` | Recreate Database and Elasticsearch indexes (removes any existent data). | | `npm run cov` | Code Coverage Report. | @@ -337,6 +338,6 @@ When we add, update or delete models and/or endpoints we have to make sure that - Test, that when we migrate DB from the previous state using `npm run migrate`, we get exactly the same DB schema as if we create DB from scratch using command `npm run init-db force`. ## EMSI mapping -mapping EMSI tags to topcoder skills -Run `npm run emsi-mapping` to create the mapping file +mapping EMSI tags to topcoder skills +Run `npm run emsi-mapping` to create the mapping file It will take about 15 minutes to create the mapping file `script/emsi-mapping/emsi-skils-mapping.js` diff --git a/package.json b/package.json index 31009f5f..6c47684a 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "services:up": "docker-compose -f ./local/docker-compose.yml --env-file .env up -d", "services:down": "docker-compose -f ./local/docker-compose.yml down", "services:logs": "docker-compose -f ./local/docker-compose.yml logs", + "services:rebuild": "docker-compose -f ./local/docker-compose.yml build --no-cache", "local:init": "npm run local:reset && npm run data:import -- --force", "local:reset": "npm run delete-index -- --force || true && npm run create-index -- --force && npm run init-db force", "cov": "nyc --reporter=html --reporter=text npm run test", From ff54e2adf378e8fd95502e990ef2a1bf94705314 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sat, 31 Jul 2021 11:17:57 +0300 Subject: [PATCH 12/50] fix memberRate when creating WPP ref issue #430 --- src/services/WorkPeriodPaymentService.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index 1201edac..6e78d8e0 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -61,6 +61,10 @@ async function _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking (w throw new errors.ConflictError(`id: ${correspondingResourceBooking.id} "ResourceBooking" Billing account is not assigned to the resource booking`) } workPeriodPayment.billingAccountId = correspondingResourceBooking.billingAccountId + // TODO: we should allow `memberRate` to be `null` as it's not required for additional payments + workPeriodPayment.memberRate = _.defaultTo(correspondingResourceBooking.memberRate, 0) + workPeriodPayment.customerRate = _.defaultTo(correspondingResourceBooking.customerRate, null) + if (!_.has(workPeriodPayment, 'days') || workPeriodPayment.days > 0) { if (_.isNil(correspondingResourceBooking.memberRate)) { throw new errors.ConflictError(`Can't find a member rate in ResourceBooking: ${correspondingResourceBooking.id} to calculate the amount`) @@ -83,9 +87,7 @@ async function _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking (w workPeriodPayment.days = _.defaultTo(workPeriodPayment.days, maxPossibleDays) workPeriodPayment.amount = _.round(workPeriodPayment.memberRate * workPeriodPayment.days / 5, 2) } - // TODO: we should allow `memberRate` to be `null` as it's not required for additional payments - workPeriodPayment.memberRate = _.defaultTo(correspondingResourceBooking.memberRate, 0) - workPeriodPayment.customerRate = _.defaultTo(correspondingResourceBooking.customerRate, null) + workPeriodPayment.id = uuid.v4() workPeriodPayment.status = WorkPeriodPaymentStatus.SCHEDULED workPeriodPayment.createdBy = createdBy From 21ae6fad0e14046c91d783ce9aa4f1d6031476bd Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sat, 31 Jul 2021 11:19:48 +0300 Subject: [PATCH 13/50] remove commented code ref issue #428 --- src/services/ResourceBookingService.js | 12 ------------ src/services/WorkPeriodService.js | 5 ----- 2 files changed, 17 deletions(-) diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 294f4c2d..a69d205f 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -250,18 +250,6 @@ async function _ensurePaidWorkPeriodsNotDeleted (resourceBookingId, oldValue, ne // we can't delete workperiods with paymentStatus 'partially-completed', 'completed' or 'in-progress', // or any of it's WorkPeriodsPayment has status 'completed' or 'in-progress'. _checkForPaidWorkPeriods(workPeriodsToRemove) - // check if this update makes maximum possible daysWorked value less than daysPaid - /* https://github.com/topcoder-platform/taas-apis/issues/428 - _.each(newWorkPeriods, newWP => { - const wp = _.find(workPeriods, ['startDate', newWP.startDate]) - if (!wp) { - return - } - if (wp.daysPaid > newWP.daysWorked) { - throw new errors.ConflictError(`Cannot make maximum daysWorked (${newWP.daysWorked}) to the value less than daysPaid (${wp.daysPaid}) for WorkPeriod: ${wp.id}`) - } - }) - */ } /** diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index 702161a5..aa061d5d 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -273,11 +273,6 @@ async function updateWorkPeriod (currentUser, id, data) { if (_.isNil(thisWeek)) { throw new errors.ConflictError('Work Period dates are not compatible with Resource Booking dates') } - /* https://github.com/topcoder-platform/taas-apis/issues/428 - if (thisWeek.daysWorked < data.daysWorked) { - throw new errors.BadRequestError(`Maximum allowed daysWorked is (${thisWeek.daysWorked})`) - } - */ data.paymentStatus = helper.calculateWorkPeriodPaymentStatus(_.assign({}, oldValue, data)) data.updatedBy = await helper.getUserId(currentUser.userId) const updated = await workPeriod.update(data) From 407326b16f708a862dc76ce17d84dc4e9d586890 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sat, 31 Jul 2021 12:06:08 +0300 Subject: [PATCH 14/50] fix edit payments fix postman collection after merging fix maxPossibleDays calculation don't update amount in challenge as it is not allowed by Challenge API ref issue #431 --- ...opcoder-bookings-api.postman_collection.json | 2 +- src/services/WorkPeriodPaymentService.js | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 3c71cdb1..ce9b9b17 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "8ead1433-9679-46de-9baa-d27d59106673" + "_postman_id": "8ead1433-9679-46de-9baa-d27d59106673", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index ea003f1f..6e5b8b36 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -56,20 +56,15 @@ async function _createSingleWorkPeriodPayment (workPeriodPayment, createdBy) { */ async function _updateChallenge (challengeId, data) { const body = {} + if (data.billingAccountId) { body.billing = { billingAccountId: _.toString(data.billingAccountId), markup: 0 // for TaaS payments we always use 0 markup } } - if (data.amount) { - body.prizeSets = [{ - type: 'placement', - prizes: [{ type: 'USD', value: data.amount }] - }] - } - if (data.billingAccountId || data.amount) { + if (data.billingAccountId) { try { await helper.updateChallenge(challengeId, body) logger.debug({ component: 'WorkPeriodPaymentService', context: 'updateChallenge', message: `Challenge with id ${challengeId} is updated` }) @@ -279,9 +274,9 @@ async function updateWorkPeriodPayment (currentUser, id, data) { if (data.days) { const correspondingWorkPeriod = await helper.ensureWorkPeriodById(workPeriodPayment.workPeriodId) // ensure work period exists - const maxPossibleDays = correspondingWorkPeriod.daysWorked - correspondingWorkPeriod.daysPaid - oldValue.days + const maxPossibleDays = correspondingWorkPeriod.daysWorked - (correspondingWorkPeriod.daysPaid - oldValue.days) if (data.days > maxPossibleDays) { - throw new errors.BadRequestError(`Days cannot be more than not paid days which is ${maxPossibleDays}`) + throw new errors.BadRequestError(`Cannot update days paid to more than ${maxPossibleDays}, otherwise total paid days (${correspondingWorkPeriod.daysPaid - oldValue.days}) would be more that total worked days (${correspondingWorkPeriod.daysWorked}) for the week.`) } } @@ -312,8 +307,8 @@ partiallyUpdateWorkPeriodPayment.schema = Joi.object().keys({ id: Joi.string().uuid().required(), data: Joi.object().keys({ status: Joi.workPeriodPaymentUpdateStatus(), - amount: Joi.number().min(0), - days: Joi.number().integer(), + amount: Joi.number().greater(0), + days: Joi.number().integer().min(0).max(10), memberRate: Joi.number().positive(), customerRate: Joi.number().positive().allow(null), billingAccountId: Joi.number().positive().integer() From 2d8680b0602499d6c326e36b39ff5c74e7d7b034 Mon Sep 17 00:00:00 2001 From: darkrider97 Date: Sat, 31 Jul 2021 17:48:26 +0530 Subject: [PATCH 15/50] Challenge: 430559a5-abf5-4820-ade1-2c8a38b951fd Implemented email notifications --- .gitignore | 3 + README.md | 1 + app-constants.js | 12 +- app.js | 7 + config/default.js | 22 +- config/email_template.config.js | 42 ++ data/notifications.json | 232 +++++++++++ data/template.html | 385 ++++++++++++++++++ package-lock.json | 46 ++- package.json | 3 + scripts/notification-renderer/index.js | 25 ++ src/services/EmailNotificationService.js | 473 +++++++++++++++++++++++ 12 files changed, 1246 insertions(+), 5 deletions(-) create mode 100644 data/notifications.json create mode 100644 data/template.html create mode 100644 scripts/notification-renderer/index.js create mode 100644 src/services/EmailNotificationService.js diff --git a/.gitignore b/.gitignore index ea148e00..ee81ead3 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,6 @@ api.env # macOS files .DS_Store + +# rendered html for email template +scripts/notification-renderer/rendered.html diff --git a/README.md b/README.md index be6ef755..6bd78f40 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,7 @@ To be able to change and test `taas-es-processor` locally you can follow the nex | `npm run delete-index` | Delete Elasticsearch indexes. Use `-- --force` flag to skip confirmation | | `npm run data:import ` | Imports data into ES and db from filePath (`./data/demo-data.json` is used as default). Use `-- --force` flag to skip confirmation | | `npm run data:export ` | Exports data from ES and db into filePath (`./data/demo-data.json` is used as default). Use `-- --force` flag to skip confirmation | +| `npm run renderTemplate ` | Generates `scripts/notification-renderer/rendered.html` which has the rendered email template for the given `notificationId` where `notificationId` is one of the keys in `data/notifications.json` ex: `npm run renderTemplate upcomingResourceBookingExpiration` | | `npm run index:all` | Indexes all data from db into ES. Use `-- --force` flag to skip confirmation | | `npm run index:jobs ` | Indexes job data from db into ES, if jobId is not given all data is indexed. Use `-- --force` flag to skip confirmation | | `npm run index:job-candidates ` | Indexes job candidate data from db into ES, if jobCandidateId is not given all data is indexed. Use `-- --force` flag to skip confirmation | diff --git a/app-constants.js b/app-constants.js index 83dcbdb2..78bab1f3 100644 --- a/app-constants.js +++ b/app-constants.js @@ -152,6 +152,14 @@ const PaymentSchedulerStatus = { CLOSE_CHALLENGE: 'close-challenge' } +const JobStatus = { + OPEN: 'open' +} + +const JobCandidateStatus = { + INTERVIEW: 'interview' +} + module.exports = { UserRoles, FullManagePermissionRoles, @@ -164,5 +172,7 @@ module.exports = { PaymentSchedulerStatus, PaymentProcessingSwitch, PaymentStatusRules, - ActiveWorkPeriodPaymentStatuses + ActiveWorkPeriodPaymentStatuses, + JobStatus, + JobCandidateStatus } diff --git a/app.js b/app.js index e6d79c69..1078645d 100644 --- a/app.js +++ b/app.js @@ -14,6 +14,7 @@ const logger = require('./src/common/logger') const eventHandlers = require('./src/eventHandlers') const interviewService = require('./src/services/InterviewService') const { processScheduler } = require('./src/services/PaymentSchedulerService') +const emailNotificationService = require('./src/services/EmailNotificationService') // setup express app const app = express() @@ -101,6 +102,12 @@ const server = app.listen(app.get('port'), () => { // schedule payment processing schedule.scheduleJob(config.PAYMENT_PROCESSING.CRON, processScheduler) + + schedule.scheduleJob(config.CRON_CANDIDATE_REVIEW, emailNotificationService.sendCandidatesAvailableEmails) + schedule.scheduleJob(config.CRON_INTERVIEW_COMING_UP, emailNotificationService.sendInterviewComingUpEmails) + schedule.scheduleJob(config.CRON_INTERVIEW_COMPLETED, emailNotificationService.sendInterviewCompletedEmails) + schedule.scheduleJob(config.CRON_POST_INTERVIEW, emailNotificationService.sendPostInterviewActionEmails) + schedule.scheduleJob(config.CRON_UPCOMING_RESOURCE_BOOKING, emailNotificationService.sendResourceBookingExpirationEmails) }) if (process.env.NODE_ENV === 'test') { diff --git a/config/default.js b/config/default.js index cb589290..5d47ba96 100644 --- a/config/default.js +++ b/config/default.js @@ -226,5 +226,25 @@ module.exports = { interview: 'withdrawn', selected: 'withdrawn', offered: 'withdrawn' - } + }, + // the sender email + NOTIFICATION_SENDER_EMAIL: process.env.NOTIFICATION_SENDER_EMAIL, + // the email notification sendgrid template id + NOTIFICATION_SENDGRID_TEMPLATE_ID: process.env.NOTIFICATION_SENDGRID_TEMPLATE_ID, + // hours after interview completed when we should post the notification + INTERVIEW_COMPLETED_NOTIFICATION_HOURS: process.env.INTERVIEW_COMPLETED_NOTIFICATION_HOURS || 4, + // no of weeks before expiry when we should post the notification + RESOURCE_BOOKING_EXPIRY_NOTIFICATION_WEEKS: process.env.RESOURCE_BOOKING_EXPIRY_NOTIFICATION_WEEKS || 3, + // frequency of cron checking for available candidates for review + CRON_CANDIDATE_REVIEW: process.env.CRON_CANDIDATE_REVIEW || '00 00 13 * * 0-6', + // frequency of cron checking for coming up interviews + // when changing this to frequency other than 5 mins, please change the minutesRange in sendInterviewComingUpEmails correspondingly + CRON_INTERVIEW_COMING_UP: process.env.CRON_INTERVIEW_COMING_UP || '*/5 * * * *', + // frequency of cron checking for interview completed + // when changing this to frequency other than 5 mins, please change the minutesRange in sendInterviewCompletedEmails correspondingly + CRON_INTERVIEW_COMPLETED: process.env.CRON_INTERVIEW_COMPLETED || '*/5 * * * *', + // frequency of cron checking for post interview actions + CRON_POST_INTERVIEW: process.env.CRON_POST_INTERVIEW || '00 00 13 * * 0-6', + // frequency of cron checking for upcoming resource bookings + CRON_UPCOMING_RESOURCE_BOOKING: process.env.CRON_UPCOMING_RESOURCE_BOOKING || '00 00 13 * * 1' } diff --git a/config/email_template.config.js b/config/email_template.config.js index e223ce95..5bc21291 100644 --- a/config/email_template.config.js +++ b/config/email_template.config.js @@ -100,5 +100,47 @@ module.exports = { cc: config.INTERVIEW_INVITATION_CC_LIST, recipients: config.INTERVIEW_INVITATION_RECIPIENTS_LIST, sendgridTemplateId: config.INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID + }, + 'candidate-review': { + subject: 'Topcoder - {{teamName}} has job candidates available for review', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'interview-coming-up-host': { + subject: 'Topcoder - Interview Coming Up: {{jobTitle}} with {{guestFullName}}', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'interview-coming-up-guest': { + subject: 'Topcoder - Interview Coming Up: {{jobTitle}} with {{hostFullName}}', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'interview-completed': { + subject: 'Topcoder - Interview Awaits Resolution: {{jobTitle}} for {{guestFullName}}', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'post-interview-action': { + subject: 'Topcoder - Candidate Action Required in {{teamName}} for {{numCandidates}} candidates', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'resource-booking-expiration': { + subject: 'Topcoder - Resource Booking Expiring in {{teamName}} for {{numResourceBookings}} resource bookings', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID } } diff --git a/data/notifications.json b/data/notifications.json new file mode 100644 index 00000000..98c19b5f --- /dev/null +++ b/data/notifications.json @@ -0,0 +1,232 @@ +{ + "candidatesAvailableForReview": { + "teamName": "the thing three", + "teamJobs": [ + { + "title": "Dummy title - at most 64 characters", + "nResourceBookings": 0, + "jobCandidates": [ + { + "handle": "testfordevemail", + "name": "John Doe", + "status": "open" + } + ], + "reviewLink": "https://platform.topcoder-dev.com/taas/myteams/111/positions/36dad9f2-98ed-4d3a-9ea7-2cd3d0f8a51a/candidates/to-review" + }, + { + "title": "Dummy title - at most 64 characters", + "nResourceBookings": 0, + "jobCandidates": [ + { + "handle": "pshah_manager", + "name": "pshah manager", + "status": "open" + }, + { + "handle": "pshah_manager", + "name": "pshah manager", + "status": "open" + }, + { + "handle": "pshah_manager", + "name": "pshah manager", + "status": "open" + }, + { + "handle": "pshah_manager", + "name": "pshah manager", + "status": "open" + }, + { + "handle": "pshah_manager", + "name": "pshah manager", + "status": "open" + }, + { + "handle": "pshah_manager", + "name": "pshah manager", + "status": "open" + }, + { + "handle": "testfordevemail", + "name": "John Doe", + "status": "open" + } + ], + "reviewLink": "https://platform.topcoder-dev.com/taas/myteams/111/positions/728ff056-63f6-4730-8a9f-3074acad8479/candidates/to-review" + } + ], + "notificationType": { + "candidatesAvailableForReview": true + }, + "description": "Candidates are available for review", + "subject": "Topcoder - the thing three has job candidates available for review", + "body": "" + }, + "interviewComingUpForHost": { + "jobTitle": "Dummy title - at most 64 characters", + "guestFullName": "name1", + "hostFullName": "host name", + "candidateName": "John Doe", + "handle": "testfordevemail", + "attendees": [ + "name1", + "name2", + "name3" + ], + "startTime": "Tue, 27 Jul 2021 21:21:17 GMT", + "duration": 120, + "interviewLink": "https://platform.topcoder-dev.com/taas/myteams/111/positions/36dad9f2-98ed-4d3a-9ea7-2cd3d0f8a51a/candidates/interviews", + "notificationType": { + "interviewComingUpForHost": true + }, + "description": "Interview Coming Up", + "subject": "Topcoder - Interview Coming Up: Dummy title - at most 64 characters with name1", + "body": "" + }, + + "interviewComingUpForGuest": { + "jobTitle": "Dummy title - at most 64 characters", + "guestFullName": "name1", + "hostFullName": "host name", + "candidateName": "John Doe", + "handle": "testfordevemail", + "attendees": [ + "name1", + "name2", + "name3" + ], + "startTime": "Tue, 27 Jul 2021 21:21:17 GMT", + "duration": 120, + "interviewLink": "https://platform.topcoder-dev.com/taas/myteams/111/positions/36dad9f2-98ed-4d3a-9ea7-2cd3d0f8a51a/candidates/interviews", + "notificationType": { + "interviewComingUpForGuest": true + }, + "description": "Interview Coming Up", + "subject": "Topcoder - Interview Coming Up: Dummy title - at most 64 characters with host name", + "body": "" + }, + + "interviewCompleted": { + "jobTitle": "Dummy title - at most 64 characters", + "guestFullName": "name1", + "hostFullName": null, + "candidateName": "John Doe", + "handle": "testfordevemail", + "attendees": [ + "name1", + "name2", + "name3" + ], + "startTime": "Tue, 27 Jul 2021 21:21:17 GMT", + "duration": 120, + "interviewLink": "https://platform.topcoder-dev.com/taas/myteams/111/positions/36dad9f2-98ed-4d3a-9ea7-2cd3d0f8a51a/candidates/interviews", + "notificationType": { + "interviewCompleted": true + }, + "description": "Interview Completed", + "subject": "Topcoder - Interview Awaits Resolution: Dummy title - at most 64 characters for name1", + "body": "" + }, + + "postInterviewCandidateAction": { + "teamName": "the thing three", + "numCandidates": 3, + "teamInterviews": [ + { + "jobTitle": "Dummy title - at most 64 characters", + "guestFullName": "name1", + "hostFullName": "host name", + "candidateName": "John Doe", + "handle": "testfordevemail", + "attendees": [ + "name1", + "name2", + "name3" + ], + "startTime": "Tue, 27 Jul 2021 21:21:17 GMT", + "duration": 120, + "interviewLink": "https://platform.topcoder-dev.com/taas/myteams/111/positions/36dad9f2-98ed-4d3a-9ea7-2cd3d0f8a51a/candidates/interviews" + }, + { + "jobTitle": "Dummy title - at most 64 characters", + "guestFullName": "guest1", + "hostFullName": null, + "candidateName": "pshah manager", + "handle": "pshah_manager", + "attendees": [ + "guest1", + "guest2", + "guest3" + ], + "startTime": "Tue, 27 Jul 2021 21:21:17 GMT", + "duration": 30, + "interviewLink": "https://platform.topcoder-dev.com/taas/myteams/111/positions/728ff056-63f6-4730-8a9f-3074acad8479/candidates/interviews" + }, + { + "jobTitle": "Dummy title - at most 64 characters", + "guestFullName": "g name1", + "hostFullName": null, + "candidateName": "John Doe", + "handle": "testfordevemail", + "attendees": [ + "g name1", + "g name2" + ], + "startTime": "Tue, 27 Jul 2021 21:21:17 GMT", + "duration": 60, + "interviewLink": "https://platform.topcoder-dev.com/taas/myteams/111/positions/728ff056-63f6-4730-8a9f-3074acad8479/candidates/interviews" + } + ], + "notificationType": { + "postInterviewCandidateAction": true + }, + "description": "Post Interview Candidate Action Reminder", + "subject": "Topcoder - Candidate Action Required in the thing three for 3 candidates", + "body": "" + }, + + "upcomingResourceBookingExpiration": { + "teamName": "the thing three", + "numResourceBookings": 6, + "teamResourceBookings": [ + { + "jobTitle": "Dummy title - at most 64 characters", + "handle": "testcat", + "endDate": "2021-04-27" + }, + { + "jobTitle": "Dummy title - at most 64 characters", + "handle": "sachin-wipro", + "endDate": "2020-10-27" + }, + { + "jobTitle": "Dummy title - at most 64 characters", + "handle": "aaaa", + "endDate": "2021-04-27" + }, + { + "jobTitle": "Dummy title - at most 64 characters", + "handle": "pshah_manager", + "endDate": "2021-01-27" + }, + { + "jobTitle": "Dummy title - at most 64 characters", + "handle": "amy_admin", + "endDate": "2021-06-15" + }, + { + "jobTitle": "Dummy title - at most 64 characters", + "handle": "lakshmiaconnmgr", + "endDate": "2021-06-15" + } + ], + "notificationType": { + "upcomingResourceBookingExpiration": true + }, + "description": "Upcoming Resource Booking Expiration", + "subject": "Topcoder - Resource Booking Expiring in the thing three for 6 resource bookings", + "body": "" + } +} diff --git a/data/template.html b/data/template.html new file mode 100644 index 00000000..7dcb8f30 --- /dev/null +++ b/data/template.html @@ -0,0 +1,385 @@ +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + + + +
IMG +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ Topcoder Gig Work
+
+ + + + + + + + + +
IMG + {{description}} +
+
+ + + + + + +
+ + + + + + + + +
+ + {{#if notificationType.candidatesAvailableForReview}} + + + + + + + + {{#each teamJobs}} + + + + + + + {{/each}} +
Job titleNo of resource bookingsCandidatesStatus
{{this.title}}{{this.nResourceBookings}} + + +
    + {{#each this.jobCandidates}} +
  • {{this.status}}
  • + {{/each}} +
+
+ {{/if}} + + {{#if notificationType.interviewComingUpForHost}} + + + + + + + + + + + + + + + +
Job titleCandidate HandleInterviewsAttendeesDate and Time
{{this.jobTitle}}{{this.handle}} + Link +
    + {{#each this.attendees}} +
  • {{this}}
  • + {{/each}} +
+
{{this.startTime}}
+ {{/if}} + + {{#if notificationType.interviewComingUpForGuest}} + + + + + + + + + + + +
Job titleDate and TimeInterviews
{{this.jobTitle}}{{this.startTime}} + Link
+ {{/if}} + + {{#if notificationType.interviewCompleted}} + + + + + + + + + + + + + + + +
Job titleCandidate HandleDate and TimeAttendeesInterviews
{{this.jobTitle}}{{this.handle}}{{this.startTime}} +
    + {{#each this.attendees}} +
  • {{this}}
  • + {{/each}} +
+
+ Link
+ {{/if}} + + {{#if notificationType.postInterviewCandidateAction}} + + + + + + + + + {{#each teamInterviews}} + + + + + + + + {{/each}} +
Job titleHandleDate and TimeAttendeesInterviews
{{this.jobTitle}}{{this.handle}}{{this.startTime}} +
    + {{#each this.attendees}} +
  • {{this}}
  • + {{/each}} +
+
+ Link
+ {{/if}} + + {{#if notificationType.upcomingResourceBookingExpiration}} + Team Name: + {{teamName}} + + + + + + + {{#each teamResourceBookings}} + + + + + + {{/each}} +
Job titleResource Bookings HandleEnd Date
{{this.jobTitle}}{{this.handle}}{{this.endDate}}
+ {{/if}} + +
+ If you have any questions about this process or if you encounter any issues coordinating your availability, you may reply to this email or send a separate email to our Gig Work operations team. + +

Thanks!
+ The Topcoder Team +

+
+
+
+ + + + + + +
+
+ + + + + + +
+
+
+ + + + + + +
+
+ + + + + + + + + + + + + + + + + +
+ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ 201 S Capitol Ave #1100
+ Indianapolis, IN 46225 United States
+
+ ●●●
+
+ Topcoder System Information: +
+ InterviewType: {{xai_template}}
+
+ + + + + + +
+
+
diff --git a/package-lock.json b/package-lock.json index 3d174e12..dce8640e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3232,6 +3232,27 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -4935,9 +4956,9 @@ } }, "moment": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.0.tgz", - "integrity": "sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA==" + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" }, "moment-timezone": { "version": "0.5.33", @@ -5014,6 +5035,12 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -7489,6 +7516,13 @@ "is-typedarray": "^1.0.0" } }, + "uglify-js": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.0.tgz", + "integrity": "sha512-R/tiGB1ZXp2BC+TkRGLwj8xUZgdfT2f4UZEgX6aVjJ5uttPrr4fYmwTWDGqVnBCLbOXRMY6nr/BTbwCtVfps0g==", + "dev": true, + "optional": true + }, "umzug": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", @@ -7886,6 +7920,12 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, "workerpool": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz", diff --git a/package.json b/package.json index 17c1887b..20faf890 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "index:roles": "node scripts/es/reIndexRoles.js", "data:export": "node scripts/data/exportData.js", "data:import": "node scripts/data/importData.js", + "renderTemplate": "node scripts/notification-renderer/index.js", "migrate": "npx sequelize db:migrate", "migrate:undo": "npx sequelize db:migrate:undo", "test": "mocha test/unit/*.test.js --timeout 30000 --require test/prepare.js --exit", @@ -51,6 +52,7 @@ "http-status-codes": "^2.1.4", "joi": "^17.2.1", "lodash": "^4.17.20", + "moment": "^2.29.1", "node-schedule": "^2.0.0", "pg": "^8.4.0", "pg-hstore": "^2.3.3", @@ -66,6 +68,7 @@ "devDependencies": { "chai": "^4.2.0", "csv-parser": "^3.0.0", + "handlebars": "^4.7.7", "mocha": "^8.1.3", "nodemon": "^2.0.4", "nyc": "^15.1.0", diff --git a/scripts/notification-renderer/index.js b/scripts/notification-renderer/index.js new file mode 100644 index 00000000..f3c1193b --- /dev/null +++ b/scripts/notification-renderer/index.js @@ -0,0 +1,25 @@ +/** + * Script for rendering email template + */ +const fs = require('fs') +const Handlebars = require('handlebars') +const path = require('path') + +function render (filename, data) { + const source = fs.readFileSync(filename, 'utf8').toString() + const template = Handlebars.compile(source) + const output = template(data) + return output +} + +const data = JSON.parse(fs.readFileSync(path.join(__dirname, '../../data/notifications.json'), 'utf8')) + +const key = process.argv.length >= 3 ? process.argv[2] : 'candidatesAvailableForReview' + +if (!data[key]) { + console.error('Please provide a proper key which is present in notifications.json') + process.exit(1) +} + +const result = render(path.join(__dirname, '../../data/template.html'), data[key]) +fs.writeFileSync(path.join(__dirname, 'rendered.html'), result) diff --git a/src/services/EmailNotificationService.js b/src/services/EmailNotificationService.js new file mode 100644 index 00000000..b27b6a48 --- /dev/null +++ b/src/services/EmailNotificationService.js @@ -0,0 +1,473 @@ +/** + * Email notification service - has the cron handlers for sending different types of email notifications + */ +const _ = require('lodash') +const { Op } = require('sequelize') +const moment = require('moment') +const config = require('config') +const models = require('../models') +const Job = models.Job +const JobCandidate = models.JobCandidate +const Interview = models.Interview +const ResourceBooking = models.ResourceBooking +const helper = require('../common/helper') +const teamService = require('./TeamService') +const constants = require('../../app-constants') +const logger = require('../common/logger') + +const localLogger = { + debug: (message, context) => logger.debug({ component: 'EmailNotificationService', context, message }), + error: (message, context) => logger.error({ component: 'EmailNotificationService', context, message }), + info: (message, context) => logger.info({ component: 'EmailNotificationService', context, message }) +} + +/** + * Returns the project with the given id + * @param projectId the project id + * @returns the project + */ +async function getProjectWithId (projectId) { + let project = null + try { + project = await helper.getProjectById(helper.getAuditM2Muser(), projectId) + } catch (err) { + localLogger.error( + `exception fetching project with id: ${projectId} Status Code: ${err.status} message: ${err.response.text}`, 'getProjectWithId') + } + + return project +} + +/** + * extract the members emails from the given project + * @param project the project + * @returns {string[]} array of emails + */ +function getProjectMembersEmails (project) { + let recipientEmails = _.map(_.get(project, 'members', []), member => member.email) + recipientEmails = _.filter(recipientEmails, email => email) + if (_.isEmpty(recipientEmails)) { + localLogger.error(`No recipients for projectId:${project.id}`, 'getProjectMembersEmails') + } + + return recipientEmails +} + +/** + * Gets the user with the given id + * @param userId the user id + * @returns the user + */ +async function getUserWithId (userId) { + let user = null + try { + user = await helper.ensureUserById(userId) + } catch (err) { + localLogger.error( + `exception fetching user with id: ${userId} Status Code: ${err.status} message: ${err.response.text}`, 'getUserWithId') + } + + return user +} + +/** + * returns the data for the interview + * @param interview the interview + * @param jobCandidate optional jobCandidate corresponding to interview + * @param job option job corresponding to interview + * @returns the interview details in format used by client + */ +async function getDataForInterview (interview, jobCandidate, job) { + jobCandidate = jobCandidate || await JobCandidate.findById(interview.jobCandidateId) + + job = job || await Job.findById(jobCandidate.jobId) + + const user = await getUserWithId(jobCandidate.userId) + if (!user) { return null } + + const interviewLink = `${config.TAAS_APP_URL}/${job.projectId}/positions/${job.id}/candidates/interviews` + const guestName = _.isEmpty(interview.guestNames) ? '' : interview.guestNames[0] + const startTime = _.isEmpty(interview.startTimestamp) ? '' : interview.startTimestamp.toUTCString() + + return { + jobTitle: job.title, + guestFullName: guestName, + hostFullName: interview.hostName, + candidateName: `${user.firstName} ${user.lastName}`, + handle: user.handle, + attendees: interview.guestNames, + startTime: startTime, + duration: interview.duration, + interviewLink + } +} + +/** + * Sends email notifications to all the teams which have candidates available for review + */ +async function sendCandidatesAvailableEmails () { + const jobsDao = await Job.findAll({ + include: [{ + model: JobCandidate, + as: 'candidates', + required: true, + where: { + status: constants.JobStatus.OPEN + } + }] + }) + const jobs = _.map(jobsDao, dao => dao.dataValues) + + const projectIds = _.uniq(_.map(jobs, job => job.projectId)) + // for each unique project id, send an email + for (const projectId of projectIds) { + const project = await getProjectWithId(projectId) + if (!project) { continue } + + const recipientEmails = getProjectMembersEmails(project) + if (_.isEmpty(recipientEmails)) { continue } + + const projectJobs = _.filter(jobs, job => job.projectId === projectId) + + const teamJobs = [] + for (const projectJob of projectJobs) { + // get candidate list + const jobCandidates = [] + for (const jobCandidate of projectJob.candidates) { + const user = await getUserWithId(jobCandidate.userId) + if (!user) { continue } + + jobCandidates.push({ + handle: user.handle, + status: jobCandidate.status + }) + } + + // review link + const reviewLink = `${config.TAAS_APP_URL}/${projectId}/positions/${projectJob.id}/candidates/to-review` + + // get # of resource bookings + const nResourceBookings = await ResourceBooking.count({ + where: { + jobId: projectJob.id + } + }) + + teamJobs.push({ + title: projectJob.title, + nResourceBookings, + jobCandidates, + reviewLink + }) + } + + teamService.sendEmail({}, { + template: 'candidate-review', + recipients: recipientEmails, + data: { + teamName: project.name, + teamJobs, + notificationType: { + candidatesAvailableForReview: true + }, + description: 'Candidates are available for review' + } + }) + } +} + +/** + * Sends email reminders to the hosts and guests about their upcoming interview(s) + */ +async function sendInterviewComingUpEmails () { + const currentTime = moment.utc() + const minutesRange = 5 + + const oneDayFromNow = currentTime.clone().add(24, 'hours') + const dayEndTime = oneDayFromNow.clone().add(minutesRange, 'minutes') + + const oneHourFromNow = currentTime.clone().add(1, 'hour') + const hourEndTime = oneHourFromNow.clone().add(minutesRange, 'minutes') + const filter = { + [Op.and]: [ + { + status: { [Op.eq]: constants.Interviews.Status.Scheduled } + }, + { + startTimestamp: { + [Op.or]: [ + { + [Op.and]: [ + { + [Op.gt]: oneDayFromNow + }, + { + [Op.lte]: dayEndTime + } + ] + }, + { + [Op.and]: [ + { + [Op.gt]: oneHourFromNow + }, + { + [Op.lte]: hourEndTime + } + ] + } + ] + } + } + ] + } + + const interviews = await Interview.findAll({ + where: filter, + raw: true + }) + + for (const interview of interviews) { + // send host email + const data = await getDataForInterview(interview) + if (!data) { continue } + + if (!_.isEmpty(interview.hostEmail)) { + teamService.sendEmail({}, { + template: 'interview-coming-up-host', + recipients: [interview.hostEmail], + data: { + ...data, + notificationType: { + interviewComingUpForHost: true + }, + description: 'Interview Coming Up' + } + }) + } else { + localLogger.error(`Interview id: ${interview.id} host email not present`, 'sendInterviewComingUpEmails') + } + + if (!_.isEmpty(interview.guestEmails)) { + // send guest emails + teamService.sendEmail({}, { + template: 'interview-coming-up-guest', + recipients: interview.guestEmails, + data: { + ...data, + notificationType: { + interviewComingUpForGuest: true + }, + description: 'Interview Coming Up' + } + }) + } else { + localLogger.error(`Interview id: ${interview.id} guest emails not present`, 'sendInterviewComingUpEmails') + } + } +} + +/** + * Sends email reminder to the interview host after it ends to change the interview status + */ +async function sendInterviewCompletedEmails () { + const minutesRange = 5 + const hoursBeforeNow = moment.utc().subtract(config.INTERVIEW_COMPLETED_NOTIFICATION_HOURS, 'hours') + const endTime = hoursBeforeNow.clone().add(minutesRange, 'minutes') + const filter = { + [Op.and]: [ + { + status: { [Op.eq]: constants.Interviews.Status.Scheduled } + }, + { + endTimestamp: { + [Op.and]: [ + { + [Op.gte]: hoursBeforeNow + }, + { + [Op.lt]: endTime + } + ] + } + } + ] + } + + const interviews = await Interview.findAll({ + where: filter, + raw: true + }) + + for (const interview of interviews) { + if (_.isEmpty(interview.hostEmail)) { + localLogger.error(`Interview id: ${interview.id} host email not present`) + continue + } + + const data = await getDataForInterview(interview) + if (!data) { continue } + + teamService.sendEmail({}, { + template: 'interview-completed', + recipients: [interview.hostEmail], + data: { + ...data, + notificationType: { + interviewCompleted: true + }, + description: 'Interview Completed' + } + }) + } +} + +/** + * Sends email reminder to the all members of teams which have interview completed to take action + * to update the job candidate status + */ +async function sendPostInterviewActionEmails () { + const completedJobCandidates = await JobCandidate.findAll({ + where: { + status: constants.JobCandidateStatus.INTERVIEW + }, + include: [{ + model: Interview, + as: 'interviews', + required: true, + where: { + status: constants.Interviews.Status.Completed + } + }] + }) + + // get all project ids for this job candidates + const jobs = await Job.findAll({ + where: { + id: { + [Op.in]: completedJobCandidates.map(jc => jc.jobId) + } + }, + raw: true + }) + + const projectIds = _.uniq(_.map(jobs, job => job.projectId)) + for (const projectId of projectIds) { + const project = await getProjectWithId(projectId) + if (!project) { continue } + + const recipientEmails = getProjectMembersEmails(project) + if (_.isEmpty(recipientEmails)) { continue } + + const projectJobs = _.filter(jobs, job => job.projectId === projectId) + const teamInterviews = [] + let numCandidates = 0 + for (const projectJob of projectJobs) { + const projectJcs = _.filter(completedJobCandidates, jc => jc.jobId === projectJob.id) + numCandidates += projectJcs.length + for (const projectJc of projectJcs) { + for (const interview of projectJc.interviews) { + const d = await getDataForInterview(interview, projectJc, projectJob) + if (!d) { continue } + teamInterviews.push(d) + } + } + } + + teamService.sendEmail({}, { + template: 'post-interview-action', + recipients: recipientEmails, + data: { + teamName: project.name, + numCandidates, + teamInterviews, + notificationType: { + postInterviewCandidateAction: true + }, + description: 'Post Interview Candidate Action Reminder' + } + }) + } +} + +/** + * Sends reminder emails to all members of teams which have atleast one upcoming resource booking expiration + */ +async function sendResourceBookingExpirationEmails () { + const currentTime = moment.utc() + const maxEndDate = currentTime.clone().add(config.RESOURCE_BOOKING_EXPIRY_NOTIFICATION_WEEKS, 'weeks') + const expiringResourceBookings = await ResourceBooking.findAll({ + where: { + endDate: { + [Op.and]: [ + { + [Op.gt]: currentTime + }, + { + [Op.lte]: maxEndDate + } + ] + } + }, + raw: true + }) + + const jobs = await Job.findAll({ + where: { + id: { + [Op.in]: _.map(expiringResourceBookings, rb => rb.jobId) + } + }, + raw: true + }) + const projectIds = _.uniq(_.map(expiringResourceBookings, rb => rb.projectId)) + + for (const projectId of projectIds) { + const project = await getProjectWithId(projectId) + if (!project) { continue } + const recipientEmails = getProjectMembersEmails(project) + if (_.isEmpty(recipientEmails)) { continue } + + const projectJobs = _.filter(jobs, job => job.projectId === projectId) + + let numResourceBookings = 0 + const teamResourceBookings = [] + for (const projectJob of projectJobs) { + const resBookings = _.filter(expiringResourceBookings, rb => rb.jobId === projectJob.id) + numResourceBookings += resBookings.length + + for (const booking of resBookings) { + const user = await getUserWithId(booking.userId) + if (!user) { continue } + + teamResourceBookings.push({ + jobTitle: projectJob.title, + handle: user.handle, + endDate: booking.endDate + }) + } + } + + teamService.sendEmail({}, { + template: 'resource-booking-expiration', + recipients: recipientEmails, + data: { + teamName: project.name, + numResourceBookings, + teamResourceBookings, + notificationType: { + upcomingResourceBookingExpiration: true + }, + description: 'Upcoming Resource Booking Expiration' + } + }) + } +} + +module.exports = { + sendCandidatesAvailableEmails, + sendInterviewComingUpEmails, + sendInterviewCompletedEmails, + sendPostInterviewActionEmails, + sendResourceBookingExpirationEmails +} From 72c995ca3639e7376357e1a1dd7aa9868a307932 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Sun, 1 Aug 2021 17:36:19 +0300 Subject: [PATCH 16/50] endpoints for create/update WPP in bulk --- ...coder-bookings-api.postman_collection.json | 207 +++++++++++++++++- docs/swagger.yaml | 136 +++++++++++- .../WorkPeriodPaymentController.js | 20 ++ src/routes/WorkPeriodPaymentRoutes.js | 14 ++ src/services/WorkPeriodPaymentService.js | 122 +++++++---- test/unit/WorkPeriodPaymentService.test.js | 2 +- 6 files changed, 455 insertions(+), 46 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index ce9b9b17..9aec690c 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "8ead1433-9679-46de-9baa-d27d59106673", + "_postman_id": "7954a27f-3833-404f-9e55-6016a938c86e", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -15940,8 +15940,12 @@ " pm.response.to.have.status(200);\r", " if(pm.response.status === \"OK\"){\r", " const response = pm.response.json()\r", - " pm.environment.set(\"workPeriodPaymentId-2\", response[0].id);\r", - " pm.environment.set(\"workPeriodPaymentId-3\", response[1].id);\r", + " if (response[0].id) {\r", + " pm.environment.set(\"workPeriodPaymentId-2\", response[0].id);\r", + " }\r", + " if (response[1].id) {\r", + " pm.environment.set(\"workPeriodPaymentId-3\", response[1].id);\r", + " }\r", " }\r", "});" ], @@ -15968,12 +15972,13 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments", + "raw": "{{URL}}/work-period-payments/bulk", "host": [ "{{URL}}" ], "path": [ - "work-period-payments" + "work-period-payments", + "bulk" ] } }, @@ -16498,7 +16503,7 @@ "response": [] }, { - "name": "create work period payment with invalid days 2 Copy", + "name": "create work period payment with invalid days 3", "event": [ { "listen": "test", @@ -17680,6 +17685,196 @@ } }, "response": [] + }, + { + "name": "patch work period payment in bulk", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "[\r\n {\r\n \"id\": \"{{workPeriodPaymentId}}\",\r\n \"status\": \"cancelled\",\r\n \"days\": 5,\r\n \"amount\": 10,\r\n \"memberRate\": 2,\r\n \"customerRate\": null,\r\n \"billingAccountId\": 44\r\n },\r\n {\r\n \"id\": \"{{workPeriodPaymentId-2}}\",\r\n \"status\": \"scheduled\"\r\n },\r\n {\r\n \"id\": \"{{workPeriodPaymentId-3}}\",\r\n \"days\": 5,\r\n \"amount\": 10,\r\n \"memberRate\": 2,\r\n \"customerRate\": 5,\r\n \"billingAccountId\": 44\r\n },\r\n {\r\n \"id\": \"{{workPeriodPaymentIdCreatedByM2M}}\",\r\n \"days\": 3\r\n }\r\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments/bulk", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments", + "bulk" + ] + } + }, + "response": [] + }, + { + "name": "patch work period payment in bulk invalid parameters - 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayments[0].status\\\" must be one of [scheduled, cancelled]\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "[\r\n {\r\n \"id\": \"{{workPeriodPaymentId}}\",\r\n \"status\": \"completed\"\r\n }\r\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments/bulk", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments", + "bulk" + ] + } + }, + "response": [] + }, + { + "name": "patch work period payment in bulk invalid parameters - 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayments[0].days\\\" must be less than or equal to 10\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "[\r\n {\r\n \"id\": \"{{workPeriodPaymentId}}\",\r\n \"days\": 11\r\n }\r\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments/bulk", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments", + "bulk" + ] + } + }, + "response": [] + }, + { + "name": "patch work period payment in bulk invalid parameters - 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayments[0].amount\\\" must be greater than 0\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "[\r\n {\r\n \"id\": \"{{workPeriodPaymentId}}\",\r\n \"amount\": 0\r\n }\r\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments/bulk", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments", + "bulk" + ] + } + }, + "response": [] } ] }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 143756d7..241135ed 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2573,6 +2573,107 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /work-period-payments/bulk: + post: + tags: + - WorkPeriodPayments + description: | + Create Work Period Payments in Bulk. + + **Authorization** Topcoder token with write Work period payment scope is allowed + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/WorkPeriodPaymentCreateRequestBody" + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + oneOf: + - $ref: "#/components/schemas/WorkPeriodPayment" + - $ref: "#/components/schemas/WorkPeriodPaymentCreatedError" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + patch: + tags: + - WorkPeriodPayments + description: | + Partial Update work period payments in bulk. + + **Authorization** Topcoder token with update work period payment scope is allowed + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/WorkPeriodPaymentPatchRequestBodyInBulk" + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + oneOf: + - $ref: "#/components/schemas/WorkPeriodPayment" + - $ref: "#/components/schemas/WorkPeriodPaymentUpdatedError" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /taas-teams: get: tags: @@ -4833,10 +4934,15 @@ components: description: "The work period id." days: type: integer - minimum: 1 - maximum: 5 + minimum: 0 + maximum: 10 example: 2 description: "The workDays to be paid." + amount: + type: integer + minimum: 1 + example: 200 + description: "The amount to be paid. Required only if days value is 0, otherwise forbidden." WorkPeriodPaymentQueryCreateRequestBody: properties: status: @@ -4932,6 +5038,32 @@ components: format: float example: 2 description: "The amount to be paid." + WorkPeriodPaymentPatchRequestBodyInBulk: + allOf: + - type: object + required: + - id + properties: + id: + type: string + format: uuid + description: "The work period payment id." + - $ref: "#/components/schemas/WorkPeriodPaymentPatchRequestBody" + WorkPeriodPaymentUpdatedError: + allOf: + - $ref: "#/components/schemas/WorkPeriodPaymentPatchRequestBodyInBulk" + - type: object + properties: + error: + type: object + properties: + message: + type: string + description: "The error message" + code: + type: integer + example: 429 + description: "HTTP code of error" CheckRun: type: object properties: diff --git a/src/controllers/WorkPeriodPaymentController.js b/src/controllers/WorkPeriodPaymentController.js index 3ac5c2fe..1fc44f26 100644 --- a/src/controllers/WorkPeriodPaymentController.js +++ b/src/controllers/WorkPeriodPaymentController.js @@ -22,6 +22,24 @@ async function createWorkPeriodPayment (req, res) { res.send(await service.createWorkPeriodPayment(req.authUser, req.body)) } +/** + * Create workPeriodPayments in bulk + * @param req the request + * @param res the response + */ +async function createBulkOfWorkPeriodPayments (req, res) { + res.send(await service.createBulkOfWorkPeriodPayments(req.authUser, req.body)) +} + +/** + * Update workPeriodPayments in bulk + * @param req the request + * @param res the response + */ +async function updateBulkOfWorkPeriodPayments (req, res) { + res.send(await service.updateBulkOfWorkPeriodPayments(req.authUser, req.body)) +} + /** * Partially update workPeriodPayment by id * @param req the request @@ -54,6 +72,8 @@ async function createQueryWorkPeriodPayments (req, res) { module.exports = { getWorkPeriodPayment, createWorkPeriodPayment, + createBulkOfWorkPeriodPayments, + updateBulkOfWorkPeriodPayments, createQueryWorkPeriodPayments, partiallyUpdateWorkPeriodPayment, searchWorkPeriodPayments diff --git a/src/routes/WorkPeriodPaymentRoutes.js b/src/routes/WorkPeriodPaymentRoutes.js index 7ddd2bc5..06377a4d 100644 --- a/src/routes/WorkPeriodPaymentRoutes.js +++ b/src/routes/WorkPeriodPaymentRoutes.js @@ -18,6 +18,20 @@ module.exports = { scopes: [constants.Scopes.READ_WORK_PERIOD_PAYMENT, constants.Scopes.ALL_WORK_PERIOD_PAYMENT] } }, + '/work-period-payments/bulk': { + post: { + controller: 'WorkPeriodPaymentController', + method: 'createBulkOfWorkPeriodPayments', + auth: 'jwt', + scopes: [constants.Scopes.CREATE_WORK_PERIOD_PAYMENT, constants.Scopes.ALL_WORK_PERIOD_PAYMENT] + }, + patch: { + controller: 'WorkPeriodPaymentController', + method: 'updateBulkOfWorkPeriodPayments', + auth: 'jwt', + scopes: [constants.Scopes.UPDATE_WORK_PERIOD_PAYMENT, constants.Scopes.ALL_WORK_PERIOD_PAYMENT] + } + }, '/work-period-payments/query': { post: { controller: 'WorkPeriodPaymentController', diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index 6e5b8b36..ac0fbe19 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -13,7 +13,7 @@ const helper = require('../common/helper') const logger = require('../common/logger') const errors = require('../common/errors') const models = require('../models') -const { WorkPeriodPaymentStatus } = require('../../app-constants') +const { WorkPeriodPaymentStatus, ActiveWorkPeriodPaymentStatuses } = require('../../app-constants') const { searchResourceBookings } = require('./ResourceBookingService') const WorkPeriodPayment = models.WorkPeriodPayment @@ -200,20 +200,7 @@ async function createWorkPeriodPayment (currentUser, workPeriodPayment) { _checkUserPermissionForCRUWorkPeriodPayment(currentUser) const createdBy = await helper.getUserId(currentUser.userId) - if (_.isArray(workPeriodPayment)) { - const result = [] - for (const wp of workPeriodPayment) { - try { - const successResult = await _createSingleWorkPeriodPayment(wp, createdBy) - result.push(successResult) - } catch (e) { - result.push(_.extend(_.pick(wp, 'workPeriodId'), { error: { message: e.message, code: e.httpStatus } })) - } - } - return result - } else { - return await _createSingleWorkPeriodPayment(workPeriodPayment, createdBy) - } + return await _createSingleWorkPeriodPayment(workPeriodPayment, createdBy) } const singleCreateWorkPeriodPaymentSchema = Joi.object().keys({ @@ -226,26 +213,48 @@ const singleCreateWorkPeriodPaymentSchema = Joi.object().keys({ }), otherwise: Joi.forbidden() }) -}) +}).required() + createWorkPeriodPayment.schema = Joi.object().keys({ currentUser: Joi.object().required(), - workPeriodPayment: Joi.alternatives().try( - singleCreateWorkPeriodPaymentSchema.required(), - Joi.array().min(1).items(singleCreateWorkPeriodPaymentSchema).required() - ).required() + workPeriodPayment: singleCreateWorkPeriodPaymentSchema +}) + +/** + * Create workPeriodPayments in bulk + * @param {Object} currentUser the user who perform this operation + * @param {Array} workPeriodPayments the workPeriodPayment to be created + * @returns {Array} the created workPeriodPayments + */ +async function createBulkOfWorkPeriodPayments (currentUser, workPeriodPayments) { + // check permission + _checkUserPermissionForCRUWorkPeriodPayment(currentUser) + const createdBy = await helper.getUserId(currentUser.userId) + + const result = [] + for (const wp of workPeriodPayments) { + try { + const successResult = await _createSingleWorkPeriodPayment(wp, createdBy) + result.push(successResult) + } catch (e) { + result.push(_.extend(_.pick(wp, 'workPeriodId'), { error: { message: e.message, code: e.httpStatus } })) + } + } + return result +} + +createBulkOfWorkPeriodPayments.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + workPeriodPayments: Joi.array().min(1).items(singleCreateWorkPeriodPaymentSchema).required() }).required() /** * Update workPeriodPayment - * @param {Object} currentUser the user who perform this operation * @param {String} id the workPeriod id * @param {Object} data the data to be updated * @returns {Object} the updated workPeriodPayment */ -async function updateWorkPeriodPayment (currentUser, id, data) { - // check permission - _checkUserPermissionForCRUWorkPeriodPayment(currentUser) - +async function updateWorkPeriodPayment (id, data) { const workPeriodPayment = await WorkPeriodPayment.findById(id) const oldValue = workPeriodPayment.toJSON() @@ -274,9 +283,11 @@ async function updateWorkPeriodPayment (currentUser, id, data) { if (data.days) { const correspondingWorkPeriod = await helper.ensureWorkPeriodById(workPeriodPayment.workPeriodId) // ensure work period exists - const maxPossibleDays = correspondingWorkPeriod.daysWorked - (correspondingWorkPeriod.daysPaid - oldValue.days) + const maxPossibleDays = correspondingWorkPeriod.daysWorked - (correspondingWorkPeriod.daysPaid - + (_.includes(ActiveWorkPeriodPaymentStatuses, oldValue.status) ? oldValue.days : 0)) if (data.days > maxPossibleDays) { - throw new errors.BadRequestError(`Cannot update days paid to more than ${maxPossibleDays}, otherwise total paid days (${correspondingWorkPeriod.daysPaid - oldValue.days}) would be more that total worked days (${correspondingWorkPeriod.daysWorked}) for the week.`) + throw new errors.BadRequestError(`Cannot update days paid to more than ${maxPossibleDays}, otherwise total paid days (${correspondingWorkPeriod.daysPaid - + (_.includes(ActiveWorkPeriodPaymentStatuses, oldValue.status) ? oldValue.days : 0)}) would be more that total worked days (${correspondingWorkPeriod.daysWorked}) for the week.`) } } @@ -285,7 +296,6 @@ async function updateWorkPeriodPayment (currentUser, id, data) { await _updateChallenge(workPeriodPayment.challengeId, data) } - data.updatedBy = await helper.getUserId(currentUser.userId) const updated = await workPeriodPayment.update(data) await helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue, key: `workPeriodPayment.billingAccountId:${updated.billingAccountId}` }) return updated.dataValues @@ -299,20 +309,56 @@ async function updateWorkPeriodPayment (currentUser, id, data) { * @returns {Object} the updated workPeriodPayment */ async function partiallyUpdateWorkPeriodPayment (currentUser, id, data) { - return updateWorkPeriodPayment(currentUser, id, data) + // check permission + _checkUserPermissionForCRUWorkPeriodPayment(currentUser) + data.updatedBy = await helper.getUserId(currentUser.userId) + return updateWorkPeriodPayment(id, data) } +const updateWorkPeriodPaymentSchema = Joi.object().keys({ + status: Joi.workPeriodPaymentUpdateStatus(), + amount: Joi.number().greater(0), + days: Joi.number().integer().min(0).max(10), + memberRate: Joi.number().positive(), + customerRate: Joi.number().positive().allow(null), + billingAccountId: Joi.number().positive().integer() +}).min(1).required() + partiallyUpdateWorkPeriodPayment.schema = Joi.object().keys({ currentUser: Joi.object().required(), id: Joi.string().uuid().required(), - data: Joi.object().keys({ - status: Joi.workPeriodPaymentUpdateStatus(), - amount: Joi.number().greater(0), - days: Joi.number().integer().min(0).max(10), - memberRate: Joi.number().positive(), - customerRate: Joi.number().positive().allow(null), - billingAccountId: Joi.number().positive().integer() - }).min(1).required() + data: updateWorkPeriodPaymentSchema +}).required() + +/** + * Partially update workPeriodPayment in bulk + * @param {Object} currentUser the user who perform this operation + * @param {Array} workPeriodPayments the workPeriodPayments data to be updated + * @returns {Array} the updated workPeriodPayment + */ +async function updateBulkOfWorkPeriodPayments (currentUser, workPeriodPayments) { + // check permission + _checkUserPermissionForCRUWorkPeriodPayment(currentUser) + const updatedBy = await helper.getUserId(currentUser.userId) + const result = [] + for (const wpp of workPeriodPayments) { + try { + const successResult = await updateWorkPeriodPayment(wpp.id, _.assign(_.omit(wpp, 'id'), { updatedBy })) + result.push(successResult) + } catch (e) { + result.push(_.assign(wpp, { error: { message: e.message, code: e.httpStatus } })) + } + } + return result +} + +updateBulkOfWorkPeriodPayments.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + workPeriodPayments: Joi.array().min(1).items( + updateWorkPeriodPaymentSchema.keys({ + id: Joi.string().uuid().required() + }).min(2).required() + ).required() }).required() /** @@ -524,7 +570,9 @@ createQueryWorkPeriodPayments.schema = Joi.object().keys({ module.exports = { getWorkPeriodPayment, createWorkPeriodPayment, + createBulkOfWorkPeriodPayments, createQueryWorkPeriodPayments, partiallyUpdateWorkPeriodPayment, + updateBulkOfWorkPeriodPayments, searchWorkPeriodPayments } diff --git a/test/unit/WorkPeriodPaymentService.test.js b/test/unit/WorkPeriodPaymentService.test.js index 0b5a8aca..b860bd22 100644 --- a/test/unit/WorkPeriodPaymentService.test.js +++ b/test/unit/WorkPeriodPaymentService.test.js @@ -7,7 +7,7 @@ const commonData = require('./common/CommonData') const testData = require('./common/WorkPeriodPaymentData') const helper = require('../../src/common/helper') const busApiClient = helper.getBusApiClient() -describe('workPeriod service test', () => { +describe('workPeriodPayment service test', () => { beforeEach(() => { sinon.stub(busApiClient, 'postEvent').callsFake(async () => {}) }) From 9a85ee2fa87f0ace3eacbd834bc663a098b3924f Mon Sep 17 00:00:00 2001 From: eisbilir Date: Mon, 2 Aug 2021 10:52:52 +0300 Subject: [PATCH 17/50] fix: swagger request for bulk update --- docs/swagger.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 241135ed..65d420f1 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2638,7 +2638,9 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/WorkPeriodPaymentPatchRequestBodyInBulk" + type: array + items: + $ref: "#/components/schemas/WorkPeriodPaymentPatchRequestBodyInBulk" responses: "200": description: OK From d3b60400cf0f07165576b4e45adf198c6f4b7f38 Mon Sep 17 00:00:00 2001 From: yoution Date: Tue, 3 Aug 2021 10:42:10 +0800 Subject: [PATCH 18/50] fix: workly-surveys --- ...21-07-26-add-send-weekly-survery-fields.js | 5 +- package.json | 1 - .../data/updateWorkPeriodSentSurveyField.js | 68 ------------------- src/common/surveyMonkey.js | 4 +- src/models/WorkPeriod.js | 2 +- src/services/SurveyService.js | 37 +++++----- 6 files changed, 24 insertions(+), 93 deletions(-) delete mode 100644 scripts/data/updateWorkPeriodSentSurveyField.js diff --git a/migrations/2021-07-26-add-send-weekly-survery-fields.js b/migrations/2021-07-26-add-send-weekly-survery-fields.js index 025d79d9..06a45672 100644 --- a/migrations/2021-07-26-add-send-weekly-survery-fields.js +++ b/migrations/2021-07-26-add-send-weekly-survery-fields.js @@ -1,4 +1,5 @@ const config = require('config') +const moment = require('moment') module.exports = { up: async (queryInterface, Sequelize) => { @@ -22,6 +23,8 @@ module.exports = { type: Sequelize.STRING(255) }, }), allowNull: true }, { transaction }) + await queryInterface.sequelize.query(`UPDATE ${config.DB_SCHEMA_NAME}.work_periods SET sent_survey = true where payment_status = 'completed' and end_date <= '${moment().subtract(7, 'days').format('YYYY-MM-DD')}'`, + { transaction }) await transaction.commit() } catch (err) { await transaction.rollback() @@ -32,7 +35,7 @@ module.exports = { const transaction = await queryInterface.sequelize.transaction() try { await queryInterface.removeColumn({ tableName: 'resource_bookings', schema: config.DB_SCHEMA_NAME }, 'send_weekly_survey', { transaction }) - await queryInterface.removeColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'send_survey', { transaction }) + await queryInterface.removeColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'sent_survey', { transaction }) await queryInterface.removeColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'sent_survey_error', { transaction } ) await transaction.commit() } catch (err) { diff --git a/package.json b/package.json index 70b1913c..17c1887b 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "index:roles": "node scripts/es/reIndexRoles.js", "data:export": "node scripts/data/exportData.js", "data:import": "node scripts/data/importData.js", - "data:workperiod": "node scripts/data/updateWorkPeriodSentSurveyField.js", "migrate": "npx sequelize db:migrate", "migrate:undo": "npx sequelize db:migrate:undo", "test": "mocha test/unit/*.test.js --timeout 30000 --require test/prepare.js --exit", diff --git a/scripts/data/updateWorkPeriodSentSurveyField.js b/scripts/data/updateWorkPeriodSentSurveyField.js deleted file mode 100644 index e641b177..00000000 --- a/scripts/data/updateWorkPeriodSentSurveyField.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * update WorkPeriod field `sentSurvey=true` - */ -const _ = require('lodash') -const moment = require('moment') -const logger = require('../../src/common/logger') -const { Op } = require('sequelize') -const models = require('../../src/models') - -const ResourceBooking = models.ResourceBooking -const WorkPeriod = models.WorkPeriod - -async function updateWorkPeriod () { - const transaction = await models.sequelize.transaction() - try { - // Start a transaction - const queryCriteria = { - attributes: ['sendWeeklySurvey', 'id'], - include: [{ - as: 'workPeriods', - model: WorkPeriod, - required: true, - where: { - [Op.and]: [ - { sentSurveyError: null }, - { sentSurvey: false }, - { paymentStatus: ['completed'] }, - { endDate: { [Op.lte]: moment().subtract(7, 'days').format('YYYY-MM-DD') } } - ] - } - }], - where: { - [Op.and]: [{ sendWeeklySurvey: true }] - }, - transaction - } - - const resourceBookings = await ResourceBooking.findAll(queryCriteria) - - _.forEach(resourceBookings, r => { - _.forEach(r.workPeriods, async w => { - // await w.update({sentSurvey: true}, {transaction: transaction} ) - await w.update({ sentSurvey: true }) - }) - }) - - // commit transaction only if all things went ok - logger.info({ - component: 'importData', - message: 'committing transaction to database...' - }) - await transaction.commit() - } catch (error) { - // logger.error({ - // component: 'importData', - // message: `Error while writing data of model: WorkPeriod` - // }) - // rollback all insert operations - if (transaction) { - logger.info({ - component: 'importData', - message: 'rollback database transaction...' - }) - transaction.rollback() - } - } -} -updateWorkPeriod() diff --git a/src/common/surveyMonkey.js b/src/common/surveyMonkey.js index c3a73499..5a65b0c8 100644 --- a/src/common/surveyMonkey.js +++ b/src/common/surveyMonkey.js @@ -57,7 +57,7 @@ function getSingleItem (lst, errorMessage) { * format `Week Ending yyyy-nth(weeks)` */ function getCollectorName (dt) { - return 'Week Ending ' + moment(dt).year() + '-' + moment(dt).format('ww') + return 'Week Ending ' + moment(dt).format('M/D/YYYY') } /* @@ -123,7 +123,6 @@ async function cloneCollector () { */ async function renameCollector (collectorId, name) { const body = { name: name } - // http.patch(BASE_URL + '/collectors/' + collectorId, body); const url = `${config.WEEKLY_SURVEY.BASE_URL}/collectors/${collectorId}` try { const response = await request @@ -147,7 +146,6 @@ async function createMessage (collectorId) { from_collector_id: `${config.WEEKLY_SURVEY.SURVEY_MASTER_COLLECTOR_ID}`, from_message_id: `${config.WEEKLY_SURVEY.SURVEY_MASTER_MESSAGE_ID}` } - // response = http.post(BASE_URL + '/collectors/' + collectorId + '/messages', body); const url = `${config.WEEKLY_SURVEY.BASE_URL}/collectors/${collectorId}/messages` try { const response = await request diff --git a/src/models/WorkPeriod.js b/src/models/WorkPeriod.js index 0204d9be..d2a3b12c 100644 --- a/src/models/WorkPeriod.js +++ b/src/models/WorkPeriod.js @@ -57,7 +57,7 @@ module.exports = (sequelize) => { allowNull: false }, sentSurvey: { - field: 'send_survey', + field: 'sent_survey', type: Sequelize.BOOLEAN, defaultValue: false, allowNull: false diff --git a/src/services/SurveyService.js b/src/services/SurveyService.js index 0b80db4f..8bdca8ca 100644 --- a/src/services/SurveyService.js +++ b/src/services/SurveyService.js @@ -26,13 +26,12 @@ async function sendSurveys () { 'workPeriods.sentSurvey': false, 'workPeriods.sentSurveyError': '', jobIds: [], - page: 1, - perPage: 1000 + page: 1 } const options = { - returnAll: false, - returnFromDB: false + returnAll: true, + returnFromDB: true } try { let resourceBookings = await searchResourceBookings(currentUser, criteria, options) @@ -48,8 +47,6 @@ async function sendSurveys () { // so several WorkPeriods for the same week would be included into on collector // and gather contacts (members) from each WorkPeriods for (const workPeriod of workPeriods) { - // await partiallyUpdateWorkPeriod(currentUser, workPeriod.id, {sentSurvey: true}) - // await partiallyUpdateWorkPeriod(currentUser, workPeriod.id, {sentSurveyError: {errorCode: 23, errorMessage: "sf"}}) try { const collectorName = getCollectorName(workPeriod.endDate) @@ -86,7 +83,6 @@ async function sendSurveys () { } } emailToWorkPeriodIdMap[collectorName][resourceBookingCache[resourceBooking.userId].email] = workPeriod.id - // resourceBookingCache[resourceBooking.userId].workPeriodId = workPeriod.id collectors[collectorName].contacts.push(resourceBookingCache[resourceBooking.userId]) } catch (e) { await partiallyUpdateWorkPeriod(currentUser, workPeriod.id, { sentSurveyError: e }) @@ -105,20 +101,22 @@ async function sendSurveys () { // send surveys for (const collectorName in collectors) { const collector = collectors[collectorName] - try { - await addContactsToSurvey( - collector.collectorId, - collector.messageId, - collector.contacts - ) - await sendSurveyAPI(collector.collectorId, collector.messageId) - - for (const contactId in contactIdToWorkPeriodIdMap[collectorName]) { - await partiallyUpdateWorkPeriod(currentUser, contactIdToWorkPeriodIdMap[collectorName][contactId], { sentSurvey: true }) + if (collector.contacts.length) { + try { + await addContactsToSurvey( + collector.collectorId, + collector.messageId, + collector.contacts + ) + await sendSurveyAPI(collector.collectorId, collector.messageId) + } catch (e) { + for (const contactId in contactIdToWorkPeriodIdMap[collectorName]) { + await partiallyUpdateWorkPeriod(currentUser, contactIdToWorkPeriodIdMap[collectorName][contactId], { sentSurveyError: e }) + } + continue } - } catch (e) { for (const contactId in contactIdToWorkPeriodIdMap[collectorName]) { - await partiallyUpdateWorkPeriod(currentUser, contactIdToWorkPeriodIdMap[collectorName][contactId], { sentSurveyError: e }) + await partiallyUpdateWorkPeriod(currentUser, contactIdToWorkPeriodIdMap[collectorName][contactId], { sentSurvey: true }) } } } @@ -126,6 +124,7 @@ async function sendSurveys () { logger.info({ component: 'SurveyService', context: 'sendSurvey', message: 'send survey successfullly' }) } catch (e) { logger.error({ component: 'SurveyService', context: 'sendSurvey', message: 'Error : ' + e.message }) + throw e } } From 19a487d357dcc6e072524ba0841edb8c7867bff1 Mon Sep 17 00:00:00 2001 From: yoution Date: Wed, 4 Aug 2021 17:29:52 +0800 Subject: [PATCH 19/50] fix: add catch for partiallyUpdateWorkPeriod --- src/services/SurveyService.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/services/SurveyService.js b/src/services/SurveyService.js index 8bdca8ca..942608ed 100644 --- a/src/services/SurveyService.js +++ b/src/services/SurveyService.js @@ -111,12 +111,20 @@ async function sendSurveys () { await sendSurveyAPI(collector.collectorId, collector.messageId) } catch (e) { for (const contactId in contactIdToWorkPeriodIdMap[collectorName]) { - await partiallyUpdateWorkPeriod(currentUser, contactIdToWorkPeriodIdMap[collectorName][contactId], { sentSurveyError: e }) + try { + await partiallyUpdateWorkPeriod(currentUser, contactIdToWorkPeriodIdMap[collectorName][contactId], { sentSurveyError: e }) + } catch (e) { + logger.error({ component: 'SurveyService', context: 'sendSurvey', message: 'Error : ' + e.message }) + } } continue } for (const contactId in contactIdToWorkPeriodIdMap[collectorName]) { - await partiallyUpdateWorkPeriod(currentUser, contactIdToWorkPeriodIdMap[collectorName][contactId], { sentSurvey: true }) + try { + await partiallyUpdateWorkPeriod(currentUser, contactIdToWorkPeriodIdMap[collectorName][contactId], { sentSurvey: true }) + } catch (e) { + logger.error({ component: 'SurveyService', context: 'sendSurvey', message: 'Error : ' + e.message }) + } } } } @@ -124,7 +132,6 @@ async function sendSurveys () { logger.info({ component: 'SurveyService', context: 'sendSurvey', message: 'send survey successfullly' }) } catch (e) { logger.error({ component: 'SurveyService', context: 'sendSurvey', message: 'Error : ' + e.message }) - throw e } } From 6a6d435318f94263625107e1e928a539ce236d0f Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 4 Aug 2021 13:19:06 +0300 Subject: [PATCH 20/50] small fixes for Weekly Surveys --- src/common/surveyMonkey.js | 61 +++++++++++++++++++---------------- src/services/SurveyService.js | 47 +++++++++++++++++++-------- 2 files changed, 67 insertions(+), 41 deletions(-) diff --git a/src/common/surveyMonkey.js b/src/common/surveyMonkey.js index 5a65b0c8..705ce41b 100644 --- a/src/common/surveyMonkey.js +++ b/src/common/surveyMonkey.js @@ -28,15 +28,15 @@ const localLogger = { info: (message, context) => logger.info({ component: 'SurveyMonkeyAPI', context, message }) } -function getRemainingRequestCountMessge (response) { +function getRemainingRequestCountMessage (response) { return `today has sent ${response.header['x-ratelimit-app-global-day-limit'] - response.header['x-ratelimit-app-global-day-remaining']} requests` } -function getErrorMessage (e) { - return { - errorCode: _.get(e, 'response.body.error.http_status_code', 400), - errorMessage: _.get(e, 'response.body.error.message', 'error message') - } +function enrichErrorMessage (e) { + e.code = _.get(e, 'response.body.error.http_status_code') + e.message = _.get(e, 'response.body.error.message', e.toString()) + + return e } function getSingleItem (lst, errorMessage) { @@ -72,12 +72,13 @@ async function searchCollector (collectorName) { .set('Content-Type', 'application/json') .set('Accept', 'application/json') - localLogger.info(`URL ${url}, ${getRemainingRequestCountMessge(response)}`, 'searchCollector') + localLogger.info(`URL ${url}, ${getRemainingRequestCountMessage(response)}`, 'searchCollector') return getSingleItem(response.body.data, 'More than 1 collector found by name ' + collectorName) } catch (e) { - localLogger.error(`URL ${url} ${getErrorMessage(e)}, ${getRemainingRequestCountMessge(e.response)}`, 'searchCollector') - throw getErrorMessage(e) + const enrichedError = enrichErrorMessage(e) + localLogger.error(`URL ${url} ERROR ${enrichedError}, ${getRemainingRequestCountMessage(e.response)}`, 'searchCollector') + throw enrichedError } } @@ -110,11 +111,12 @@ async function cloneCollector () { .set('Content-Type', 'application/json') .set('Accept', 'application/json') .send(body) - localLogger.info(`URL ${url}, ${getRemainingRequestCountMessge(response)}`, 'cloneCollector') + localLogger.info(`URL ${url}, ${getRemainingRequestCountMessage(response)}`, 'cloneCollector') return response.body.id } catch (e) { - localLogger.error(`URL ${url} ${JSON.stringify(getErrorMessage(e))}, ${getRemainingRequestCountMessge(e.response)}`, 'cloneCollector') - throw getErrorMessage(e) + const enrichedError = enrichErrorMessage(e) + localLogger.error(`URL ${url} ERROR ${enrichedError}, ${getRemainingRequestCountMessage(e.response)}`, 'cloneCollector') + throw enrichedError } } @@ -131,10 +133,11 @@ async function renameCollector (collectorId, name) { .set('Content-Type', 'application/json') .set('Accept', 'application/json') .send(body) - localLogger.info(`URL ${url}, ${getRemainingRequestCountMessge(response)}`, 'renameCollector') + localLogger.info(`URL ${url}, ${getRemainingRequestCountMessage(response)}`, 'renameCollector') } catch (e) { - localLogger.error(`URL ${url} ${JSON.stringify(getErrorMessage(e))}, ${getRemainingRequestCountMessge(e.response)}`, 'renameCollector') - throw getErrorMessage(e) + const enrichedError = enrichErrorMessage(e) + localLogger.error(`URL ${url} ERROR ${enrichedError}, ${getRemainingRequestCountMessage(e.response)}`, 'renameCollector') + throw enrichedError } } @@ -154,11 +157,12 @@ async function createMessage (collectorId) { .set('Content-Type', 'application/json') .set('Accept', 'application/json') .send(body) - localLogger.info(`URL ${url}, ${getRemainingRequestCountMessge(response)}`, 'createMessage') + localLogger.info(`URL ${url}, ${getRemainingRequestCountMessage(response)}`, 'createMessage') return response.body.id } catch (e) { - localLogger.error(`URL ${url} ${JSON.stringify(getErrorMessage(e))}, ${getRemainingRequestCountMessge(e.response)}`, 'createMessage') - throw getErrorMessage(e) + const enrichedError = enrichErrorMessage(e) + localLogger.error(`URL ${url} ERROR ${enrichedError}, ${getRemainingRequestCountMessage(e.response)}`, 'createMessage') + throw enrichedError } } @@ -182,11 +186,12 @@ async function upsertContactInSurveyMonkey (list) { .set('Accept', 'application/json') .send(body) - localLogger.info(`URL ${url}, ${getRemainingRequestCountMessge(response)}`, 'upsertContactInSurveyMonkey') + localLogger.info(`URL ${url}, ${getRemainingRequestCountMessage(response)}`, 'upsertContactInSurveyMonkey') return _.concat(response.body.existing, response.body.succeeded) } catch (e) { - localLogger.error(`URL ${url} ${JSON.stringify(getErrorMessage(e))}, ${getRemainingRequestCountMessge(e.response)}`, 'createMessage') - throw getErrorMessage(e) + const enrichedError = enrichErrorMessage(e) + localLogger.error(`URL ${url} ERROR ${enrichedError}, ${getRemainingRequestCountMessage(e.response)}`, 'createMessage') + throw enrichedError } } @@ -200,11 +205,12 @@ async function addContactsToSurvey (collectorId, messageId, contactIds) { .set('Content-Type', 'application/json') .set('Accept', 'application/json') .send(body) - localLogger.info(`URL ${url}, ${getRemainingRequestCountMessge(response)}`, 'addContactsToSurvey') + localLogger.info(`URL ${url}, ${getRemainingRequestCountMessage(response)}`, 'addContactsToSurvey') return response.body.id } catch (e) { - localLogger.error(`URL ${url} ${JSON.stringify(getErrorMessage(e))}, ${getRemainingRequestCountMessge(e.response)}`, 'addContactsToSurvey') - throw getErrorMessage(e) + const enrichedError = enrichErrorMessage(e) + localLogger.error(`URL ${url} ERROR ${enrichedError}, ${getRemainingRequestCountMessage(e.response)}`, 'addContactsToSurvey') + throw enrichedError } } @@ -217,11 +223,12 @@ async function sendSurveyAPI (collectorId, messageId) { .set('Content-Type', 'application/json') .set('Accept', 'application/json') .send({}) - localLogger.info(`URL ${url}, ${getRemainingRequestCountMessge(response)}`, 'sendSurveyAPI') + localLogger.info(`URL ${url}, ${getRemainingRequestCountMessage(response)}`, 'sendSurveyAPI') return response.body.id } catch (e) { - localLogger.error(`URL ${url} ${JSON.stringify(getErrorMessage(e))}, ${getRemainingRequestCountMessge(e.response)}`, 'sendSurveyAPI') - throw getErrorMessage(e) + const enrichedError = enrichErrorMessage(e) + localLogger.error(`URL ${url} ${enrichedError}, ${getRemainingRequestCountMessage(e.response)}`, 'sendSurveyAPI') + throw enrichedError } } diff --git a/src/services/SurveyService.js b/src/services/SurveyService.js index 942608ed..33641062 100644 --- a/src/services/SurveyService.js +++ b/src/services/SurveyService.js @@ -10,6 +10,13 @@ const resourceBookingCache = {} const contactIdToWorkPeriodIdMap = {} const emailToWorkPeriodIdMap = {} +function buildSentSurveyError (e) { + return { + errorCode: _.get(e, 'code'), + errorMessage: _.get(e, 'message', e.toString()) + } +} + /** * Scheduler process entrance */ @@ -37,7 +44,7 @@ async function sendSurveys () { let resourceBookings = await searchResourceBookings(currentUser, criteria, options) resourceBookings = resourceBookings.result - logger.info({ component: 'SurveyService', context: 'sendSurvey', message: 'load workPeriod successfullly' }) + logger.info({ component: 'SurveyService', context: 'sendSurvey', message: 'load workPeriod successfully' }) const workPeriods = _.flatten(_.map(resourceBookings, 'workPeriods')) @@ -85,9 +92,18 @@ async function sendSurveys () { emailToWorkPeriodIdMap[collectorName][resourceBookingCache[resourceBooking.userId].email] = workPeriod.id collectors[collectorName].contacts.push(resourceBookingCache[resourceBooking.userId]) } catch (e) { - await partiallyUpdateWorkPeriod(currentUser, workPeriod.id, { sentSurveyError: e }) + try { + await partiallyUpdateWorkPeriod( + currentUser, + workPeriod.id, + { sentSurveyError: buildSentSurveyError(e) } + ) + } catch (e) { + logger.error({ component: 'SurveyService', context: 'sendSurvey', message: `Error updating survey as failed for Work Period "${workPeriod.id}": ` + e.message }) + } } } + // add contacts for (const collectorName in collectors) { const collector = collectors[collectorName] @@ -109,29 +125,32 @@ async function sendSurveys () { collector.contacts ) await sendSurveyAPI(collector.collectorId, collector.messageId) - } catch (e) { for (const contactId in contactIdToWorkPeriodIdMap[collectorName]) { try { - await partiallyUpdateWorkPeriod(currentUser, contactIdToWorkPeriodIdMap[collectorName][contactId], { sentSurveyError: e }) + await partiallyUpdateWorkPeriod(currentUser, contactIdToWorkPeriodIdMap[collectorName][contactId], { sentSurvey: true }) } catch (e) { - logger.error({ component: 'SurveyService', context: 'sendSurvey', message: 'Error : ' + e.message }) + logger.error({ component: 'SurveyService', context: 'sendSurvey', message: `Error updating survey as sent for Work Period "${contactIdToWorkPeriodIdMap[collectorName][contactId]}": ` + e.message }) } } - continue - } - for (const contactId in contactIdToWorkPeriodIdMap[collectorName]) { - try { - await partiallyUpdateWorkPeriod(currentUser, contactIdToWorkPeriodIdMap[collectorName][contactId], { sentSurvey: true }) - } catch (e) { - logger.error({ component: 'SurveyService', context: 'sendSurvey', message: 'Error : ' + e.message }) + } catch (e) { + for (const contactId in contactIdToWorkPeriodIdMap[collectorName]) { + try { + await partiallyUpdateWorkPeriod( + currentUser, + contactIdToWorkPeriodIdMap[collectorName][contactId], + { sentSurveyError: buildSentSurveyError(e) } + ) + } catch (e) { + logger.error({ component: 'SurveyService', context: 'sendSurvey', message: `Error updating survey as failed for Work Period "${contactIdToWorkPeriodIdMap[collectorName][contactId]}": ` + e.message }) + } } } } } - logger.info({ component: 'SurveyService', context: 'sendSurvey', message: 'send survey successfullly' }) + logger.info({ component: 'SurveyService', context: 'sendSurvey', message: 'Processing weekly surveys is completed' }) } catch (e) { - logger.error({ component: 'SurveyService', context: 'sendSurvey', message: 'Error : ' + e.message }) + logger.error({ component: 'SurveyService', context: 'sendSurvey', message: 'Error sending surveys: ' + e.message }) } } From e9c50befb0d2a99f055492605b8e1cc08e97ccce Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Wed, 4 Aug 2021 19:46:40 +0800 Subject: [PATCH 21/50] add migration scripts --- data/demo-data.json | 14 ++-- package.json | 5 +- scripts/withdrawn-migration/ReadMe.md | 17 +++++ scripts/withdrawn-migration/backup.js | 67 +++++++++++++++++++ scripts/withdrawn-migration/migration.js | 37 ++++++++++ scripts/withdrawn-migration/restore.js | 36 ++++++++++ ...21 23:53:20 GMT+0800 (GMT+08:00) copy.json | 1 + ...23 2021 23:59:35 GMT+0800 (GMT+08:00).json | 1 + ...01 2021 21:59:22 GMT+0800 (GMT+08:00).json | 1 + .../temp/jobcandidate-backup.json | 1 + 10 files changed, 172 insertions(+), 8 deletions(-) create mode 100644 scripts/withdrawn-migration/ReadMe.md create mode 100644 scripts/withdrawn-migration/backup.js create mode 100644 scripts/withdrawn-migration/migration.js create mode 100644 scripts/withdrawn-migration/restore.js create mode 100644 scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:53:20 GMT+0800 (GMT+08:00) copy.json create mode 100644 scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:59:35 GMT+0800 (GMT+08:00).json create mode 100644 scripts/withdrawn-migration/temp/jobcandidate-backup-Sun Aug 01 2021 21:59:22 GMT+0800 (GMT+08:00).json create mode 100644 scripts/withdrawn-migration/temp/jobcandidate-backup.json diff --git a/data/demo-data.json b/data/demo-data.json index 9d939c4c..187f763a 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -54,7 +54,7 @@ "isApplicationPageActive": false, "minSalary": 100, "maxSalary": 200, - "hoursPerWeek": 20, + "hoursPerWeek": 80, "jobLocation": "Any location", "jobTimezone": "GMT", "currency": "USD", @@ -765,7 +765,7 @@ "id": "b0fc417b-3f41-4c06-9f2b-8e680c3a03c6", "jobId": "728ff056-63f6-4730-8a9f-3074acad8479", "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", - "status": "open", + "status": "placed", "externalId": "300234321", "resume": "http://example.com", "remark": "excellent", @@ -793,7 +793,7 @@ "id": "02a622f4-7894-4ac0-a823-a952ffa1b3f3", "jobId": "728ff056-63f6-4730-8a9f-3074acad8479", "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", - "status": "open", + "status": "selected", "externalId": "300234321", "resume": "http://example.com", "remark": "excellent", @@ -807,7 +807,7 @@ "id": "b32b4819-7bfa-49a8-851e-69cdddff8149", "jobId": "728ff056-63f6-4730-8a9f-3074acad8479", "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", - "status": "open", + "status": "skills-test", "externalId": "300234321", "resume": "http://example.com", "remark": "excellent", @@ -1359,11 +1359,11 @@ { "id": "d6103727-6615-4168-8169-0485577bfb3f", "projectId": 111, - "userId": "bef43122-426b-4b2b-acdd-9b5b3bd1c0bf", - "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "jobId": "728ff056-63f6-4730-8a9f-3074acad8479", "status": "placed", "startDate": "2021-03-27", - "endDate": "2021-04-27", + "endDate": "2021-08-23", "memberRate": 13.23, "customerRate": 13, "rateType": "hourly", diff --git a/package.json b/package.json index 6c47684a..8811efb4 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,10 @@ "local:reset": "npm run delete-index -- --force || true && npm run create-index -- --force && npm run init-db force", "cov": "nyc --reporter=html --reporter=text npm run test", "demo-payment-scheduler": "node scripts/demo-payment-scheduler/index.js && npm run index:all -- --force", - "demo-payment": "node scripts/demo-payment" + "demo-payment": "node scripts/demo-payment", + "migrate:backup-withdrawn": "node scripts/withdrawn-migration/backup.js", + "migrate:migration-withdrawn": "node scripts/withdrawn-migration/migration.js", + "migrate:restore-withdrawn": "node scripts/withdrawn-migration/restore.js" }, "keywords": [], "author": "", diff --git a/scripts/withdrawn-migration/ReadMe.md b/scripts/withdrawn-migration/ReadMe.md new file mode 100644 index 00000000..c0719bd1 --- /dev/null +++ b/scripts/withdrawn-migration/ReadMe.md @@ -0,0 +1,17 @@ +Steps: + +1. Put the withdrawn-migration inside the scripts folder +2. Add below three commands into the package.json + + "migrate:backup-withdrawn": "node scripts/withdrawn-migration/backup.js", + "migrate:migration-withdrawn": "node scripts/withdrawn-migration/migration.js", + "migrate:restore-withdrawn": "node scripts/withdrawn-migration/restore.js" + +3. Run `npm run migrate:backup-withdrawn` - this will create a buckup file regarding the jobCandidate we will update - naming convension is `jobcandidate-backup-Fri Jul 23 2021 23:53:20 GMT+0800 (GMT+08:00).json` + +4. Double check the jobcandidate-backup-date.json file and once confirmed the backup file, rename the file into +`jobcandidate-backup.json` name + +5. Run `npm run migrate:migration-withdrawn` , make sure to keep the `jobcandidate-backup.json` file for a while, as if we need to restore the db, we will need it. + +6. We can use `npm run migrate:restore-withdrawn` \ No newline at end of file diff --git a/scripts/withdrawn-migration/backup.js b/scripts/withdrawn-migration/backup.js new file mode 100644 index 00000000..e0f4b86f --- /dev/null +++ b/scripts/withdrawn-migration/backup.js @@ -0,0 +1,67 @@ +/** + * Back up the jobCandidates that we will update it's status + */ +const config = require('config') +const Sequelize = require('sequelize') +const fs = require('fs') +const path = require('path') +const { JobCandidate, ResourceBooking, Job } = require('../../src/models') +const logger = require('../../src/common/logger') + +const currentStep = 'Backup' + +async function backup () { + logger.info({ component: currentStep, message: '*************************** Backup process started ***************************' }) + const filePath = path.join(__dirname, '/temp/') + const Op = Sequelize.Op + const jobCandidates = await JobCandidate.findAll({ + where: { + status: 'placed' + } + }) + + for (let i = 0; i < jobCandidates.length; i++) { + const jc = jobCandidates[i] + const job = await Job.findById(jc.jobId) + const rb = await ResourceBooking.findOne({ + where: { + userId: jc.userId, + jobId: jc.jobId + } + }) + let completed = false + if (rb && rb.endDate) { + completed = new Date(rb.endDate) < new Date() && new Date(rb.endDate).toDateString() !== new Date().toDateString() + } + if (job.hoursPerWeek > config.JOBS_HOUR_PER_WEEK && !completed) { + const statuses = ['applied', 'skills-test', 'phone-screen', 'open', 'interview', 'selected', 'offered'] + const filter = { [Op.and]: [] } + filter[Op.and].push({ status: statuses }) + filter[Op.and].push({ userId: jc.userId }) + const candidates = await JobCandidate.findAll({ + where: filter + }) + if (candidates && candidates.length > 0) { + fs.writeFile(filePath + `jobcandidate-backup-${new Date().toString()}.json`, JSON.stringify( + candidates + ), (err) => { + if (!err) { + logger.info({ component: `${currentStep} Summary`, message: `Backup up finished. There are ${candidates.length} jobCandidates that need to be updated` }) + logger.info({ component: currentStep, message: '*************************** Backup process finished ***************************' }) + return + } + logger.error({ component: currentStep, message: err.message }) + process.exit(1) + }) + } + } + } +} + +backup().then(() => { + logger.info({ component: currentStep, message: 'Execution Finished!' }) + process.exit() +}).catch(err => { + logger.error(err.message) + process.exit(1) +}) diff --git a/scripts/withdrawn-migration/migration.js b/scripts/withdrawn-migration/migration.js new file mode 100644 index 00000000..ff923e6f --- /dev/null +++ b/scripts/withdrawn-migration/migration.js @@ -0,0 +1,37 @@ +/** + * Migration the jobCandidate status into expected status + */ +const config = require('config') +const fs = require('fs') +const path = require('path') +const { JobCandidate } = require('../../src/models') +const logger = require('../../src/common/logger') + +const currentStep = 'Migration' + +async function migration () { + logger.info({ component: currentStep, message: '*************************** Migration process started ***************************' }) + const filePath = path.join(__dirname, '/temp/') + const data = fs.readFileSync(filePath + 'jobCandidate-backup.json', 'utf-8') + const jobCandidates = JSON.parse(data) + let summary = 0 + for (var i = 0; i < jobCandidates.length; i++) { + const jc = await JobCandidate.findById(jobCandidates[i].id) + if (jc) { + const oldStatus = jc.status + const updated = await jc.update({ status: config.WITHDRAWN_STATUS_CHANGE_MAPPING[jobCandidates[i].status] }) + summary++ + logger.info({ component: currentStep, message: `jobCandidate with ${jc.id} status changed from ${oldStatus} to ${updated.status}` }) + } + }; + logger.info({ component: currentStep, message: `Totally updated ${summary} jobCandidates` }) + logger.info({ component: currentStep, message: '*************************** Migration process finished ***************************' }) +} + +migration().then(() => { + logger.info({ component: currentStep, message: 'Execution Finished!' }) + process.exit() +}).catch(err => { + logger.error(err.message) + process.exit(1) +}) diff --git a/scripts/withdrawn-migration/restore.js b/scripts/withdrawn-migration/restore.js new file mode 100644 index 00000000..0150d75a --- /dev/null +++ b/scripts/withdrawn-migration/restore.js @@ -0,0 +1,36 @@ +/** + * Resotre the changed jobCandidates into its original state. + */ +const fs = require('fs') +const path = require('path') +const { JobCandidate } = require('../../src/models') +const logger = require('../../src/common/logger') + +const currentStep = 'Restore' + +async function restore () { + logger.info({ component: currentStep, message: '*************************** Restore process started ***************************' }) + const filePath = path.join(__dirname, '/temp/') + const data = fs.readFileSync(filePath + 'jobCandidate-backup.json', 'utf-8') + const jobCandidates = JSON.parse(data) + let summary = 0 + for (var i = 0; i < jobCandidates.length; i++) { + const jc = await JobCandidate.findById(jobCandidates[i].id) + if (jc) { + const oldStatus = jc.status + const updated = await jc.update({ status: jobCandidates[i].status }) + summary++ + logger.info({ component: currentStep, message: `jobCandidate with ${jc.id} status restored from ${oldStatus} to ${updated.status}` }) + } + }; + logger.info({ component: currentStep, message: `Totally restored ${summary} jobCandidates` }) + logger.info({ component: currentStep, message: '*************************** Restore process finished ***************************' }) +} + +restore().then(() => { + logger.info({ component: currentStep, message: 'Execution Finished!' }) + process.exit() +}).catch(err => { + logger.error(err.message) + process.exit(1) +}) diff --git a/scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:53:20 GMT+0800 (GMT+08:00) copy.json b/scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:53:20 GMT+0800 (GMT+08:00) copy.json new file mode 100644 index 00000000..cae89cec --- /dev/null +++ b/scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:53:20 GMT+0800 (GMT+08:00) copy.json @@ -0,0 +1 @@ +[{"id":"c637ecf3-8df5-42e7-80d6-daba422e371a","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:41.500Z","updatedAt":"2021-07-23T15:46:17.058Z"},{"id":"02a622f4-7894-4ac0-a823-a952ffa1b3f3","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"selected","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:43.985Z","updatedAt":"2021-07-23T15:46:17.073Z"},{"id":"b32b4819-7bfa-49a8-851e-69cdddff8149","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"skills-test","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:46.310Z","updatedAt":"2021-07-23T15:46:17.081Z"},{"id":"08a67e4d-6857-492c-a3fa-cd7c64e76a69","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:48.449Z","updatedAt":"2021-07-23T15:46:17.089Z"},{"id":"881a19de-2b0c-4bb9-b36a-4cb5e223bdb5","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:50.595Z","updatedAt":"2021-07-23T15:46:17.097Z"}] \ No newline at end of file diff --git a/scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:59:35 GMT+0800 (GMT+08:00).json b/scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:59:35 GMT+0800 (GMT+08:00).json new file mode 100644 index 00000000..182deb3f --- /dev/null +++ b/scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:59:35 GMT+0800 (GMT+08:00).json @@ -0,0 +1 @@ +[{"id":"c637ecf3-8df5-42e7-80d6-daba422e371a","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:41.500Z","updatedAt":"2021-05-09T21:14:41.500Z"},{"id":"02a622f4-7894-4ac0-a823-a952ffa1b3f3","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"selected","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:43.985Z","updatedAt":"2021-05-09T21:14:43.985Z"},{"id":"b32b4819-7bfa-49a8-851e-69cdddff8149","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"skills-test","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:46.310Z","updatedAt":"2021-05-09T21:14:46.310Z"},{"id":"08a67e4d-6857-492c-a3fa-cd7c64e76a69","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:48.449Z","updatedAt":"2021-05-09T21:14:48.449Z"},{"id":"881a19de-2b0c-4bb9-b36a-4cb5e223bdb5","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:50.595Z","updatedAt":"2021-05-09T21:14:50.595Z"}] \ No newline at end of file diff --git a/scripts/withdrawn-migration/temp/jobcandidate-backup-Sun Aug 01 2021 21:59:22 GMT+0800 (GMT+08:00).json b/scripts/withdrawn-migration/temp/jobcandidate-backup-Sun Aug 01 2021 21:59:22 GMT+0800 (GMT+08:00).json new file mode 100644 index 00000000..182deb3f --- /dev/null +++ b/scripts/withdrawn-migration/temp/jobcandidate-backup-Sun Aug 01 2021 21:59:22 GMT+0800 (GMT+08:00).json @@ -0,0 +1 @@ +[{"id":"c637ecf3-8df5-42e7-80d6-daba422e371a","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:41.500Z","updatedAt":"2021-05-09T21:14:41.500Z"},{"id":"02a622f4-7894-4ac0-a823-a952ffa1b3f3","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"selected","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:43.985Z","updatedAt":"2021-05-09T21:14:43.985Z"},{"id":"b32b4819-7bfa-49a8-851e-69cdddff8149","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"skills-test","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:46.310Z","updatedAt":"2021-05-09T21:14:46.310Z"},{"id":"08a67e4d-6857-492c-a3fa-cd7c64e76a69","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:48.449Z","updatedAt":"2021-05-09T21:14:48.449Z"},{"id":"881a19de-2b0c-4bb9-b36a-4cb5e223bdb5","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:50.595Z","updatedAt":"2021-05-09T21:14:50.595Z"}] \ No newline at end of file diff --git a/scripts/withdrawn-migration/temp/jobcandidate-backup.json b/scripts/withdrawn-migration/temp/jobcandidate-backup.json new file mode 100644 index 00000000..182deb3f --- /dev/null +++ b/scripts/withdrawn-migration/temp/jobcandidate-backup.json @@ -0,0 +1 @@ +[{"id":"c637ecf3-8df5-42e7-80d6-daba422e371a","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:41.500Z","updatedAt":"2021-05-09T21:14:41.500Z"},{"id":"02a622f4-7894-4ac0-a823-a952ffa1b3f3","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"selected","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:43.985Z","updatedAt":"2021-05-09T21:14:43.985Z"},{"id":"b32b4819-7bfa-49a8-851e-69cdddff8149","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"skills-test","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:46.310Z","updatedAt":"2021-05-09T21:14:46.310Z"},{"id":"08a67e4d-6857-492c-a3fa-cd7c64e76a69","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:48.449Z","updatedAt":"2021-05-09T21:14:48.449Z"},{"id":"881a19de-2b0c-4bb9-b36a-4cb5e223bdb5","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:50.595Z","updatedAt":"2021-05-09T21:14:50.595Z"}] \ No newline at end of file From 67d375d3b6b97f83c786aa4df401248852967bd7 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Wed, 4 Aug 2021 19:49:31 +0800 Subject: [PATCH 22/50] migration scripts --- scripts/withdrawn-migration/ReadMe.md | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 scripts/withdrawn-migration/ReadMe.md diff --git a/scripts/withdrawn-migration/ReadMe.md b/scripts/withdrawn-migration/ReadMe.md deleted file mode 100644 index c0719bd1..00000000 --- a/scripts/withdrawn-migration/ReadMe.md +++ /dev/null @@ -1,17 +0,0 @@ -Steps: - -1. Put the withdrawn-migration inside the scripts folder -2. Add below three commands into the package.json - - "migrate:backup-withdrawn": "node scripts/withdrawn-migration/backup.js", - "migrate:migration-withdrawn": "node scripts/withdrawn-migration/migration.js", - "migrate:restore-withdrawn": "node scripts/withdrawn-migration/restore.js" - -3. Run `npm run migrate:backup-withdrawn` - this will create a buckup file regarding the jobCandidate we will update - naming convension is `jobcandidate-backup-Fri Jul 23 2021 23:53:20 GMT+0800 (GMT+08:00).json` - -4. Double check the jobcandidate-backup-date.json file and once confirmed the backup file, rename the file into -`jobcandidate-backup.json` name - -5. Run `npm run migrate:migration-withdrawn` , make sure to keep the `jobcandidate-backup.json` file for a while, as if we need to restore the db, we will need it. - -6. We can use `npm run migrate:restore-withdrawn` \ No newline at end of file From 3030ac71787c9983a0845c5d07d04c903735bd72 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Wed, 4 Aug 2021 19:51:22 +0800 Subject: [PATCH 23/50] clean up the data --- .gitignore | 1 + ...ackup-Fri Jul 23 2021 23:53:20 GMT+0800 (GMT+08:00) copy.json | 1 - ...ate-backup-Fri Jul 23 2021 23:59:35 GMT+0800 (GMT+08:00).json | 1 - ...ate-backup-Sun Aug 01 2021 21:59:22 GMT+0800 (GMT+08:00).json | 1 - scripts/withdrawn-migration/temp/jobcandidate-backup.json | 1 - 5 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:53:20 GMT+0800 (GMT+08:00) copy.json delete mode 100644 scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:59:35 GMT+0800 (GMT+08:00).json delete mode 100644 scripts/withdrawn-migration/temp/jobcandidate-backup-Sun Aug 01 2021 21:59:22 GMT+0800 (GMT+08:00).json delete mode 100644 scripts/withdrawn-migration/temp/jobcandidate-backup.json diff --git a/.gitignore b/.gitignore index ea148e00..bed42869 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ build/Release # Dependency directories node_modules/ jspm_packages/ +scripts/withdrawn-migration/temp/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ diff --git a/scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:53:20 GMT+0800 (GMT+08:00) copy.json b/scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:53:20 GMT+0800 (GMT+08:00) copy.json deleted file mode 100644 index cae89cec..00000000 --- a/scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:53:20 GMT+0800 (GMT+08:00) copy.json +++ /dev/null @@ -1 +0,0 @@ -[{"id":"c637ecf3-8df5-42e7-80d6-daba422e371a","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:41.500Z","updatedAt":"2021-07-23T15:46:17.058Z"},{"id":"02a622f4-7894-4ac0-a823-a952ffa1b3f3","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"selected","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:43.985Z","updatedAt":"2021-07-23T15:46:17.073Z"},{"id":"b32b4819-7bfa-49a8-851e-69cdddff8149","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"skills-test","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:46.310Z","updatedAt":"2021-07-23T15:46:17.081Z"},{"id":"08a67e4d-6857-492c-a3fa-cd7c64e76a69","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:48.449Z","updatedAt":"2021-07-23T15:46:17.089Z"},{"id":"881a19de-2b0c-4bb9-b36a-4cb5e223bdb5","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:50.595Z","updatedAt":"2021-07-23T15:46:17.097Z"}] \ No newline at end of file diff --git a/scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:59:35 GMT+0800 (GMT+08:00).json b/scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:59:35 GMT+0800 (GMT+08:00).json deleted file mode 100644 index 182deb3f..00000000 --- a/scripts/withdrawn-migration/temp/jobcandidate-backup-Fri Jul 23 2021 23:59:35 GMT+0800 (GMT+08:00).json +++ /dev/null @@ -1 +0,0 @@ -[{"id":"c637ecf3-8df5-42e7-80d6-daba422e371a","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:41.500Z","updatedAt":"2021-05-09T21:14:41.500Z"},{"id":"02a622f4-7894-4ac0-a823-a952ffa1b3f3","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"selected","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:43.985Z","updatedAt":"2021-05-09T21:14:43.985Z"},{"id":"b32b4819-7bfa-49a8-851e-69cdddff8149","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"skills-test","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:46.310Z","updatedAt":"2021-05-09T21:14:46.310Z"},{"id":"08a67e4d-6857-492c-a3fa-cd7c64e76a69","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:48.449Z","updatedAt":"2021-05-09T21:14:48.449Z"},{"id":"881a19de-2b0c-4bb9-b36a-4cb5e223bdb5","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:50.595Z","updatedAt":"2021-05-09T21:14:50.595Z"}] \ No newline at end of file diff --git a/scripts/withdrawn-migration/temp/jobcandidate-backup-Sun Aug 01 2021 21:59:22 GMT+0800 (GMT+08:00).json b/scripts/withdrawn-migration/temp/jobcandidate-backup-Sun Aug 01 2021 21:59:22 GMT+0800 (GMT+08:00).json deleted file mode 100644 index 182deb3f..00000000 --- a/scripts/withdrawn-migration/temp/jobcandidate-backup-Sun Aug 01 2021 21:59:22 GMT+0800 (GMT+08:00).json +++ /dev/null @@ -1 +0,0 @@ -[{"id":"c637ecf3-8df5-42e7-80d6-daba422e371a","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:41.500Z","updatedAt":"2021-05-09T21:14:41.500Z"},{"id":"02a622f4-7894-4ac0-a823-a952ffa1b3f3","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"selected","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:43.985Z","updatedAt":"2021-05-09T21:14:43.985Z"},{"id":"b32b4819-7bfa-49a8-851e-69cdddff8149","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"skills-test","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:46.310Z","updatedAt":"2021-05-09T21:14:46.310Z"},{"id":"08a67e4d-6857-492c-a3fa-cd7c64e76a69","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:48.449Z","updatedAt":"2021-05-09T21:14:48.449Z"},{"id":"881a19de-2b0c-4bb9-b36a-4cb5e223bdb5","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:50.595Z","updatedAt":"2021-05-09T21:14:50.595Z"}] \ No newline at end of file diff --git a/scripts/withdrawn-migration/temp/jobcandidate-backup.json b/scripts/withdrawn-migration/temp/jobcandidate-backup.json deleted file mode 100644 index 182deb3f..00000000 --- a/scripts/withdrawn-migration/temp/jobcandidate-backup.json +++ /dev/null @@ -1 +0,0 @@ -[{"id":"c637ecf3-8df5-42e7-80d6-daba422e371a","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:41.500Z","updatedAt":"2021-05-09T21:14:41.500Z"},{"id":"02a622f4-7894-4ac0-a823-a952ffa1b3f3","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"selected","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:43.985Z","updatedAt":"2021-05-09T21:14:43.985Z"},{"id":"b32b4819-7bfa-49a8-851e-69cdddff8149","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"skills-test","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:46.310Z","updatedAt":"2021-05-09T21:14:46.310Z"},{"id":"08a67e4d-6857-492c-a3fa-cd7c64e76a69","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:48.449Z","updatedAt":"2021-05-09T21:14:48.449Z"},{"id":"881a19de-2b0c-4bb9-b36a-4cb5e223bdb5","jobId":"728ff056-63f6-4730-8a9f-3074acad8479","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"open","externalId":"300234321","resume":"http://example.com","remark":"excellent","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":null,"createdAt":"2021-05-09T21:14:50.595Z","updatedAt":"2021-05-09T21:14:50.595Z"}] \ No newline at end of file From 5d05945db803d11068d6b51586b8d0fb25f77a10 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Wed, 4 Aug 2021 20:02:10 +0800 Subject: [PATCH 24/50] remove the timestamp --- scripts/withdrawn-migration/backup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/withdrawn-migration/backup.js b/scripts/withdrawn-migration/backup.js index e0f4b86f..a83c99b4 100644 --- a/scripts/withdrawn-migration/backup.js +++ b/scripts/withdrawn-migration/backup.js @@ -42,7 +42,7 @@ async function backup () { where: filter }) if (candidates && candidates.length > 0) { - fs.writeFile(filePath + `jobcandidate-backup-${new Date().toString()}.json`, JSON.stringify( + fs.writeFile(filePath + `jobcandidate-backup.json`, JSON.stringify( candidates ), (err) => { if (!err) { From a9704355d22c317a00e809265a97794e95e6cec5 Mon Sep 17 00:00:00 2001 From: darkrider97 Date: Wed, 4 Aug 2021 17:51:00 +0530 Subject: [PATCH 25/50] 1. Making sure team service doesn't send internal notifications 2. Sending the cron email notifications to topic notifications.action.create --- config/default.js | 2 + config/email_template.config.js | 256 ++++++++++++----------- src/common/helper.js | 24 ++- src/services/EmailNotificationService.js | 52 ++++- src/services/TeamService.js | 12 +- 5 files changed, 201 insertions(+), 145 deletions(-) diff --git a/config/default.js b/config/default.js index 5d47ba96..569922ea 100644 --- a/config/default.js +++ b/config/default.js @@ -147,6 +147,8 @@ module.exports = { // the Kafka message topic for sending email EMAIL_TOPIC: process.env.EMAIL_TOPIC || 'external.action.email', + // the Kafka message topic for creating notifications + NOTIFICATIONS_CREATE_TOPIC: process.env.NOTIFICATIONS_CREATE_TOPIC || 'notifications.action.create', // the emails address for receiving the issue report // REPORT_ISSUE_EMAILS may contain comma-separated list of email which is converted to array REPORT_ISSUE_EMAILS: (process.env.REPORT_ISSUE_EMAILS || '').split(','), diff --git a/config/email_template.config.js b/config/email_template.config.js index 5bc21291..3e397864 100644 --- a/config/email_template.config.js +++ b/config/email_template.config.js @@ -12,135 +12,139 @@ module.exports = { * - projectName: the project name. Example: "TaaS API Misc Updates" * - reportText: the body of reported issue. Example: "I have issue with ... \n ... Thank you in advance!" */ - 'team-issue-report': { - subject: 'Issue Reported on TaaS Team {{projectName}} ({{projectId}}).', - body: 'Project Name: {{projectName}}' + '\n' + - 'Project ID: {{projectId}}' + '\n' + - `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + - '\n' + - '{{reportText}}', - recipients: config.REPORT_ISSUE_EMAILS, - sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID - }, + teamTemplates: { + 'team-issue-report': { + subject: 'Issue Reported on TaaS Team {{projectName}} ({{projectId}}).', + body: 'Project Name: {{projectName}}' + '\n' + + 'Project ID: {{projectId}}' + '\n' + + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + + '\n' + + '{{reportText}}', + recipients: config.REPORT_ISSUE_EMAILS, + sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID + }, - /* Report issue for a particular member - * - * - userHandle: the user handle. Example: "bili_2021" - * - projectId: the project ID. Example: 123412 - * - projectName: the project name. Example: "TaaS API Misc Updates" - * - reportText: the body of reported issue. Example: "I have issue with ... \n ... Thank you in advance!" - */ - 'member-issue-report': { - subject: 'Issue Reported for member {{userHandle}} on TaaS Team {{projectName}} ({{projectId}}).', - body: 'User Handle: {{userHandle}}' + '\n' + - 'Project Name: {{projectName}}' + '\n' + - 'Project ID: {{projectId}}' + '\n' + - `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + - '\n' + - '{{reportText}}', - recipients: config.REPORT_ISSUE_EMAILS, - sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID - }, + /* Report issue for a particular member + * + * - userHandle: the user handle. Example: "bili_2021" + * - projectId: the project ID. Example: 123412 + * - projectName: the project name. Example: "TaaS API Misc Updates" + * - reportText: the body of reported issue. Example: "I have issue with ... \n ... Thank you in advance!" + */ + 'member-issue-report': { + subject: 'Issue Reported for member {{userHandle}} on TaaS Team {{projectName}} ({{projectId}}).', + body: 'User Handle: {{userHandle}}' + '\n' + + 'Project Name: {{projectName}}' + '\n' + + 'Project ID: {{projectId}}' + '\n' + + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + + '\n' + + '{{reportText}}', + recipients: config.REPORT_ISSUE_EMAILS, + sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID + }, - /* Request extension for a particular member - * - * - userHandle: the user handle. Example: "bili_2021" - * - projectId: the project ID. Example: 123412 - * - projectName: the project name. Example: "TaaS API Misc Updates" - * - text: comment for the request. Example: "I would like to keep working with this member for 2 months..." - */ - 'extension-request': { - subject: 'Extension Requested for member {{userHandle}} on TaaS Team {{projectName}} ({{projectId}}).', - body: 'User Handle: {{userHandle}}' + '\n' + - 'Project Name: {{projectName}}' + '\n' + - 'Project ID: {{projectId}}' + '\n' + - `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + - '\n' + - '{{text}}', - recipients: config.REPORT_ISSUE_EMAILS, - sendgridTemplateId: config.REQUEST_EXTENSION_SENDGRID_TEMPLATE_ID - }, + /* Request extension for a particular member + * + * - userHandle: the user handle. Example: "bili_2021" + * - projectId: the project ID. Example: 123412 + * - projectName: the project name. Example: "TaaS API Misc Updates" + * - text: comment for the request. Example: "I would like to keep working with this member for 2 months..." + */ + 'extension-request': { + subject: 'Extension Requested for member {{userHandle}} on TaaS Team {{projectName}} ({{projectId}}).', + body: 'User Handle: {{userHandle}}' + '\n' + + 'Project Name: {{projectName}}' + '\n' + + 'Project ID: {{projectId}}' + '\n' + + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + + '\n' + + '{{text}}', + recipients: config.REPORT_ISSUE_EMAILS, + sendgridTemplateId: config.REQUEST_EXTENSION_SENDGRID_TEMPLATE_ID + }, - /* Request interview for a job candidate - * - * - interviewType: the x.ai interview type. Example: "interview-30" - * - interviewRound: the round of the interview. Example: 2 - * - interviewDuration: duration of the interview, in minutes. Example: 30 - * - interviewerList: The list of interviewer email addresses. Example: "first@attendee.com, second@attendee.com" - * - candidateId: the id of the jobCandidate. Example: "cc562545-7b75-48bf-87e7-50b3c57e41b1" - * - candidateName: Full name of candidate. Example: "John Doe" - * - jobName: The title of the job. Example: "TaaS API Misc Updates" - * - * Template (defined in SendGrid): - * Subject: '{{interviewType}} tech interview with {{candidateName}} for {{jobName}} is requested by the Customer' - * Body: - * 'Hello! - *

- * Congratulations, you have been selected to participate in a Topcoder Gig Work Interview! - *

- * Please monitor your email for a response to this where you can coordinate your availability. - *

- * Interviewee: {{candidateName}}
- * Interviewer(s): {{interviewerList}}
- * Interview Length: {{interviewDuration}} minutes - *

- * /{{interviewType}} - *

- * Topcoder Info:
- * Note: "id: {{candidateId}}, round: {{interviewRound}}"' - * - * Note, that the template should be defined in SendGrid. - * The subject & body above (identical to actual SendGrid template) is for reference purposes. - * We won't pass subject & body but only substitutions (replacements in template subject/body). - */ - 'interview-invitation': { - subject: '', - body: '', - from: config.INTERVIEW_INVITATION_SENDER_EMAIL, - cc: config.INTERVIEW_INVITATION_CC_LIST, - recipients: config.INTERVIEW_INVITATION_RECIPIENTS_LIST, - sendgridTemplateId: config.INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID - }, - 'candidate-review': { - subject: 'Topcoder - {{teamName}} has job candidates available for review', - body: '', - recipients: [], - from: config.NOTIFICATION_SENDER_EMAIL, - sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID - }, - 'interview-coming-up-host': { - subject: 'Topcoder - Interview Coming Up: {{jobTitle}} with {{guestFullName}}', - body: '', - recipients: [], - from: config.NOTIFICATION_SENDER_EMAIL, - sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID - }, - 'interview-coming-up-guest': { - subject: 'Topcoder - Interview Coming Up: {{jobTitle}} with {{hostFullName}}', - body: '', - recipients: [], - from: config.NOTIFICATION_SENDER_EMAIL, - sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID - }, - 'interview-completed': { - subject: 'Topcoder - Interview Awaits Resolution: {{jobTitle}} for {{guestFullName}}', - body: '', - recipients: [], - from: config.NOTIFICATION_SENDER_EMAIL, - sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID - }, - 'post-interview-action': { - subject: 'Topcoder - Candidate Action Required in {{teamName}} for {{numCandidates}} candidates', - body: '', - recipients: [], - from: config.NOTIFICATION_SENDER_EMAIL, - sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + /* Request interview for a job candidate + * + * - interviewType: the x.ai interview type. Example: "interview-30" + * - interviewRound: the round of the interview. Example: 2 + * - interviewDuration: duration of the interview, in minutes. Example: 30 + * - interviewerList: The list of interviewer email addresses. Example: "first@attendee.com, second@attendee.com" + * - candidateId: the id of the jobCandidate. Example: "cc562545-7b75-48bf-87e7-50b3c57e41b1" + * - candidateName: Full name of candidate. Example: "John Doe" + * - jobName: The title of the job. Example: "TaaS API Misc Updates" + * + * Template (defined in SendGrid): + * Subject: '{{interviewType}} tech interview with {{candidateName}} for {{jobName}} is requested by the Customer' + * Body: + * 'Hello! + *

+ * Congratulations, you have been selected to participate in a Topcoder Gig Work Interview! + *

+ * Please monitor your email for a response to this where you can coordinate your availability. + *

+ * Interviewee: {{candidateName}}
+ * Interviewer(s): {{interviewerList}}
+ * Interview Length: {{interviewDuration}} minutes + *

+ * /{{interviewType}} + *

+ * Topcoder Info:
+ * Note: "id: {{candidateId}}, round: {{interviewRound}}"' + * + * Note, that the template should be defined in SendGrid. + * The subject & body above (identical to actual SendGrid template) is for reference purposes. + * We won't pass subject & body but only substitutions (replacements in template subject/body). + */ + 'interview-invitation': { + subject: '', + body: '', + from: config.INTERVIEW_INVITATION_SENDER_EMAIL, + cc: config.INTERVIEW_INVITATION_CC_LIST, + recipients: config.INTERVIEW_INVITATION_RECIPIENTS_LIST, + sendgridTemplateId: config.INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID + } }, - 'resource-booking-expiration': { - subject: 'Topcoder - Resource Booking Expiring in {{teamName}} for {{numResourceBookings}} resource bookings', - body: '', - recipients: [], - from: config.NOTIFICATION_SENDER_EMAIL, - sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + 'cronEmailTemplates': { + 'candidate-review': { + subject: 'Topcoder - {{teamName}} has job candidates available for review', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'interview-coming-up-host': { + subject: 'Topcoder - Interview Coming Up: {{jobTitle}} with {{guestFullName}}', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'interview-coming-up-guest': { + subject: 'Topcoder - Interview Coming Up: {{jobTitle}} with {{hostFullName}}', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'interview-completed': { + subject: 'Topcoder - Interview Awaits Resolution: {{jobTitle}} for {{guestFullName}}', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'post-interview-action': { + subject: 'Topcoder - Candidate Action Required in {{teamName}} for {{numCandidates}} candidates', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'resource-booking-expiration': { + subject: 'Topcoder - Resource Booking Expiring in {{teamName}} for {{numResourceBookings}} resource bookings', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + } } } diff --git a/src/common/helper.js b/src/common/helper.js index 7f9625be..b5bcc756 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -22,6 +22,7 @@ const eventDispatcher = require('./eventDispatcher') const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') const moment = require('moment') const { PaymentStatusRules } = require('../../app-constants') +const emailTemplateConfig = require('../../config/email_template.config') const localLogger = { debug: (message) => @@ -2011,6 +2012,26 @@ async function getMembersSuggest (fragment) { return res.body } +/** + * Returns the email templates for given key + * @param key the type of email template ex: teamTemplates + * @returns the list of templates for the given key + */ +function getEmailTemplatesForKey (key) { + if (!_.has(emailTemplateConfig, key)) { return [] } + + return _.mapValues(emailTemplateConfig[key], (template) => { + return { + subject: template.subject, + body: template.body, + from: template.from, + recipients: template.recipients, + cc: template.cc, + sendgridTemplateId: template.sendgridTemplateId + } + }) +} + module.exports = { getParamFromCliArgs, promptUser, @@ -2072,5 +2093,6 @@ module.exports = { createProject, getMemberGroups, removeTextFormatting, - getMembersSuggest + getMembersSuggest, + getEmailTemplatesForKey } diff --git a/src/services/EmailNotificationService.js b/src/services/EmailNotificationService.js index b27b6a48..9565e72e 100644 --- a/src/services/EmailNotificationService.js +++ b/src/services/EmailNotificationService.js @@ -11,7 +11,6 @@ const JobCandidate = models.JobCandidate const Interview = models.Interview const ResourceBooking = models.ResourceBooking const helper = require('../common/helper') -const teamService = require('./TeamService') const constants = require('../../app-constants') const logger = require('../common/logger') @@ -21,6 +20,8 @@ const localLogger = { info: (message, context) => logger.info({ component: 'EmailNotificationService', context, message }) } +const emailTemplates = helper.getEmailTemplatesForKey('cronEmailTemplates') + /** * Returns the project with the given id * @param projectId the project id @@ -161,7 +162,7 @@ async function sendCandidatesAvailableEmails () { }) } - teamService.sendEmail({}, { + sendEmail({}, { template: 'candidate-review', recipients: recipientEmails, data: { @@ -233,7 +234,7 @@ async function sendInterviewComingUpEmails () { if (!data) { continue } if (!_.isEmpty(interview.hostEmail)) { - teamService.sendEmail({}, { + sendEmail({}, { template: 'interview-coming-up-host', recipients: [interview.hostEmail], data: { @@ -250,7 +251,7 @@ async function sendInterviewComingUpEmails () { if (!_.isEmpty(interview.guestEmails)) { // send guest emails - teamService.sendEmail({}, { + sendEmail({}, { template: 'interview-coming-up-guest', recipients: interview.guestEmails, data: { @@ -308,7 +309,7 @@ async function sendInterviewCompletedEmails () { const data = await getDataForInterview(interview) if (!data) { continue } - teamService.sendEmail({}, { + sendEmail({}, { template: 'interview-completed', recipients: [interview.hostEmail], data: { @@ -374,7 +375,7 @@ async function sendPostInterviewActionEmails () { } } - teamService.sendEmail({}, { + sendEmail({}, { template: 'post-interview-action', recipients: recipientEmails, data: { @@ -448,7 +449,7 @@ async function sendResourceBookingExpirationEmails () { } } - teamService.sendEmail({}, { + sendEmail({}, { template: 'resource-booking-expiration', recipients: recipientEmails, data: { @@ -464,6 +465,43 @@ async function sendResourceBookingExpirationEmails () { } } +/** + * Send email through a particular template + * @param {Object} currentUser the user who perform this operation + * @param {Object} data the email object + * @returns {undefined} + */ +async function sendEmail (currentUser, data) { + const template = emailTemplates[data.template] + const dataCC = data.cc || [] + const templateCC = template.cc || [] + const dataRecipients = data.recipients || [] + const templateRecipients = template.recipients || [] + const subjectBody = { + subject: data.subject || template.subject, + body: data.body || template.body + } + for (const key in subjectBody) { + subjectBody[key] = await helper.substituteStringByObject( + subjectBody[key], + data.data + ) + } + const emailData = { + serviceId: 'email', + type: 'taas.notification.request-submitted', + details: { + from: data.from || template.from, + recipients: _.map(_.uniq([...dataRecipients, ...templateRecipients]), function (r) { return { email: r } }), + cc: _.map(_.uniq([...dataCC, ...templateCC]), function (r) { return { email: r } }), + data: { ...data.data, ...subjectBody }, + sendgrid_template_id: template.sendgridTemplateId, + version: 'v3' + } + } + await helper.postEvent(config.NOTIFICATIONS_CREATE_TOPIC, emailData) +} + module.exports = { sendCandidatesAvailableEmails, sendInterviewComingUpEmails, diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 84b43d1f..68747541 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -6,7 +6,6 @@ const _ = require('lodash') const Joi = require('joi') const dateFNS = require('date-fns') const config = require('config') -const emailTemplateConfig = require('../../config/email_template.config') const helper = require('../common/helper') const logger = require('../common/logger') const errors = require('../common/errors') @@ -21,16 +20,7 @@ const { matchedSkills, unMatchedSkills } = require('../../scripts/emsi-mapping/e const Role = models.Role const RoleSearchRequest = models.RoleSearchRequest -const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { - return { - subject: template.subject, - body: template.body, - from: template.from, - recipients: template.recipients, - cc: template.cc, - sendgridTemplateId: template.sendgridTemplateId - } -}) +const emailTemplates = helper.getEmailTemplatesForKey('teamTemplates') /** * Function to get placed resource bookings with specific projectIds From bdaa7a27861898d95e122a37707a35f49eb28115 Mon Sep 17 00:00:00 2001 From: darkrider97 Date: Wed, 4 Aug 2021 17:56:31 +0530 Subject: [PATCH 26/50] Lint fix --- config/email_template.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/email_template.config.js b/config/email_template.config.js index 3e397864..97d5e5a2 100644 --- a/config/email_template.config.js +++ b/config/email_template.config.js @@ -103,7 +103,7 @@ module.exports = { sendgridTemplateId: config.INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID } }, - 'cronEmailTemplates': { + cronEmailTemplates: { 'candidate-review': { subject: 'Topcoder - {{teamName}} has job candidates available for review', body: '', From fb9cec3ca84d53b4564b1f8e272502050327e093 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 4 Aug 2021 15:33:15 +0300 Subject: [PATCH 27/50] configurable prefix for weekly survey collectors --- config/default.js | 3 ++- src/common/surveyMonkey.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/default.js b/config/default.js index 4675ff09..b2a51bea 100644 --- a/config/default.js +++ b/config/default.js @@ -186,9 +186,10 @@ module.exports = { BASE_URL: process.env.WEEKLY_SURVEY_BASE_URL || 'https://api.surveymonkey.net/v3/surveys', JWT_TOKEN: process.env.WEEKLY_SURVEY_JWT_TOKEN || '', SURVEY_ID: process.env.WEEKLY_SURVEY_SURVEY_ID || '', + SURVEY_COLLECTOR_PREFIX: process.env.WEEKLY_SURVEY_SURVEY_COLLECTOR_PREFIX || 'Week ending', SURVEY_MASTER_COLLECTOR_ID: process.env.WEEKLY_SURVEY_SURVEY_MASTER_COLLECTOR_ID || '', SURVEY_MASTER_MESSAGE_ID: process.env.WEEKLY_SURVEY_SURVEY_MASTER_MESSAGE_ID || '', - SURVEY_CONTACT_GROUP_ID: process.env.WEEKLY_SURVEY_SURVEY_CONTACT_GROUP_ID || '' + SURVEY_CONTACT_GROUP_ID: process.env.WEEKLY_SURVEY_SURVEY_CONTACT_GROUP_ID || '', }, // payment scheduler config PAYMENT_PROCESSING: { diff --git a/src/common/surveyMonkey.js b/src/common/surveyMonkey.js index 705ce41b..20762c3f 100644 --- a/src/common/surveyMonkey.js +++ b/src/common/surveyMonkey.js @@ -57,7 +57,7 @@ function getSingleItem (lst, errorMessage) { * format `Week Ending yyyy-nth(weeks)` */ function getCollectorName (dt) { - return 'Week Ending ' + moment(dt).format('M/D/YYYY') + return config.WEEKLY_SURVEY.SURVEY_COLLECTOR_PREFIX + ' ' + moment(dt).format('M/D/YYYY') } /* From 0daafec923d44193d38421521db0800920f3885a Mon Sep 17 00:00:00 2001 From: darkrider97 Date: Wed, 4 Aug 2021 18:44:22 +0530 Subject: [PATCH 28/50] Changing template ids --- config/email_template.config.js | 26 ++++++++++++------------ src/services/EmailNotificationService.js | 14 ++++++------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/config/email_template.config.js b/config/email_template.config.js index 97d5e5a2..f356e622 100644 --- a/config/email_template.config.js +++ b/config/email_template.config.js @@ -103,48 +103,48 @@ module.exports = { sendgridTemplateId: config.INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID } }, - cronEmailTemplates: { - 'candidate-review': { + notificationEmailTemplates: { + 'taas.notification.candidates-available-for-review': { subject: 'Topcoder - {{teamName}} has job candidates available for review', body: '', recipients: [], from: config.NOTIFICATION_SENDER_EMAIL, - sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + sendgridTemplateId: config.NOTIFICATION_SENDGRID_TEMPLATE_ID }, - 'interview-coming-up-host': { + 'taas.notification.interview-coming-up-host': { subject: 'Topcoder - Interview Coming Up: {{jobTitle}} with {{guestFullName}}', body: '', recipients: [], from: config.NOTIFICATION_SENDER_EMAIL, - sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + sendgridTemplateId: config.NOTIFICATION_SENDGRID_TEMPLATE_ID }, - 'interview-coming-up-guest': { + 'taas.notification.interview-coming-up-guest': { subject: 'Topcoder - Interview Coming Up: {{jobTitle}} with {{hostFullName}}', body: '', recipients: [], from: config.NOTIFICATION_SENDER_EMAIL, - sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + sendgridTemplateId: config.NOTIFICATION_SENDGRID_TEMPLATE_ID }, - 'interview-completed': { + 'taas.notification.interview-awaits-resolution': { subject: 'Topcoder - Interview Awaits Resolution: {{jobTitle}} for {{guestFullName}}', body: '', recipients: [], from: config.NOTIFICATION_SENDER_EMAIL, - sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + sendgridTemplateId: config.NOTIFICATION_SENDGRID_TEMPLATE_ID }, - 'post-interview-action': { + 'taas.notification.post-interview-action-required': { subject: 'Topcoder - Candidate Action Required in {{teamName}} for {{numCandidates}} candidates', body: '', recipients: [], from: config.NOTIFICATION_SENDER_EMAIL, - sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + sendgridTemplateId: config.NOTIFICATION_SENDGRID_TEMPLATE_ID }, - 'resource-booking-expiration': { + 'taas.notification.resource-booking-expiration': { subject: 'Topcoder - Resource Booking Expiring in {{teamName}} for {{numResourceBookings}} resource bookings', body: '', recipients: [], from: config.NOTIFICATION_SENDER_EMAIL, - sendgrid_template_id: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + sendgridTemplateId: config.NOTIFICATION_SENDGRID_TEMPLATE_ID } } } diff --git a/src/services/EmailNotificationService.js b/src/services/EmailNotificationService.js index 9565e72e..f0a5a892 100644 --- a/src/services/EmailNotificationService.js +++ b/src/services/EmailNotificationService.js @@ -20,7 +20,7 @@ const localLogger = { info: (message, context) => logger.info({ component: 'EmailNotificationService', context, message }) } -const emailTemplates = helper.getEmailTemplatesForKey('cronEmailTemplates') +const emailTemplates = helper.getEmailTemplatesForKey('notificationEmailTemplates') /** * Returns the project with the given id @@ -163,7 +163,7 @@ async function sendCandidatesAvailableEmails () { } sendEmail({}, { - template: 'candidate-review', + template: 'taas.notification.candidates-available-for-review', recipients: recipientEmails, data: { teamName: project.name, @@ -235,7 +235,7 @@ async function sendInterviewComingUpEmails () { if (!_.isEmpty(interview.hostEmail)) { sendEmail({}, { - template: 'interview-coming-up-host', + template: 'taas.notification.interview-coming-up-host', recipients: [interview.hostEmail], data: { ...data, @@ -252,7 +252,7 @@ async function sendInterviewComingUpEmails () { if (!_.isEmpty(interview.guestEmails)) { // send guest emails sendEmail({}, { - template: 'interview-coming-up-guest', + template: 'taas.notification.interview-coming-up-guest', recipients: interview.guestEmails, data: { ...data, @@ -310,7 +310,7 @@ async function sendInterviewCompletedEmails () { if (!data) { continue } sendEmail({}, { - template: 'interview-completed', + template: 'taas.notification.interview-awaits-resolution', recipients: [interview.hostEmail], data: { ...data, @@ -376,7 +376,7 @@ async function sendPostInterviewActionEmails () { } sendEmail({}, { - template: 'post-interview-action', + template: 'taas.notification.post-interview-action-required', recipients: recipientEmails, data: { teamName: project.name, @@ -450,7 +450,7 @@ async function sendResourceBookingExpirationEmails () { } sendEmail({}, { - template: 'resource-booking-expiration', + template: 'taas.notification.resource-booking-expiration', recipients: recipientEmails, data: { teamName: project.name, From 9c2d3683f3efbb2c3295d4cf9885582aad1a35d3 Mon Sep 17 00:00:00 2001 From: darkrider97 Date: Wed, 4 Aug 2021 18:49:03 +0530 Subject: [PATCH 29/50] Syncing notification type to template id --- src/services/EmailNotificationService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/EmailNotificationService.js b/src/services/EmailNotificationService.js index f0a5a892..5794dfa6 100644 --- a/src/services/EmailNotificationService.js +++ b/src/services/EmailNotificationService.js @@ -489,7 +489,7 @@ async function sendEmail (currentUser, data) { } const emailData = { serviceId: 'email', - type: 'taas.notification.request-submitted', + type: data.template, details: { from: data.from || template.from, recipients: _.map(_.uniq([...dataRecipients, ...templateRecipients]), function (r) { return { email: r } }), From 94632a477774ac1713e10b848b603bc75d78b864 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 4 Aug 2021 17:00:37 +0300 Subject: [PATCH 30/50] fix email notification Kafka message payload format --- local/kafka-client/topics.txt | 1 + src/services/EmailNotificationService.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/local/kafka-client/topics.txt b/local/kafka-client/topics.txt index 2611220b..405b2510 100644 --- a/local/kafka-client/topics.txt +++ b/local/kafka-client/topics.txt @@ -21,3 +21,4 @@ taas.interview.update taas.interview.bulkUpdate external.action.email taas.action.retry +notifications.action.create \ No newline at end of file diff --git a/src/services/EmailNotificationService.js b/src/services/EmailNotificationService.js index 5794dfa6..f3b9bad7 100644 --- a/src/services/EmailNotificationService.js +++ b/src/services/EmailNotificationService.js @@ -499,7 +499,9 @@ async function sendEmail (currentUser, data) { version: 'v3' } } - await helper.postEvent(config.NOTIFICATIONS_CREATE_TOPIC, emailData) + await helper.postEvent(config.NOTIFICATIONS_CREATE_TOPIC, { + notifications: [emailData] + }) } module.exports = { From 22e9ebf85a57642a34b59ba20ac95815d27468da Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Wed, 4 Aug 2021 22:08:01 +0800 Subject: [PATCH 31/50] migration scripts update --- scripts/withdrawn-migration/backup.js | 28 +++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/scripts/withdrawn-migration/backup.js b/scripts/withdrawn-migration/backup.js index a83c99b4..60eb0262 100644 --- a/scripts/withdrawn-migration/backup.js +++ b/scripts/withdrawn-migration/backup.js @@ -22,13 +22,25 @@ async function backup () { for (let i = 0; i < jobCandidates.length; i++) { const jc = jobCandidates[i] - const job = await Job.findById(jc.jobId) - const rb = await ResourceBooking.findOne({ - where: { - userId: jc.userId, - jobId: jc.jobId - } - }) + let job = null + try { + job = await Job.findById(jc.jobId) + } catch (error) { + // ignore the error + } + if (!job) continue + let rb = null + try { + rb = await ResourceBooking.findOne({ + where: { + userId: jc.userId, + jobId: jc.jobId + } + }) + } catch (error) { + // ignore the error + } + if (!rb) continue let completed = false if (rb && rb.endDate) { completed = new Date(rb.endDate) < new Date() && new Date(rb.endDate).toDateString() !== new Date().toDateString() @@ -42,7 +54,7 @@ async function backup () { where: filter }) if (candidates && candidates.length > 0) { - fs.writeFile(filePath + `jobcandidate-backup.json`, JSON.stringify( + fs.writeFile(filePath + 'jobcandidate-backup.json', JSON.stringify( candidates ), (err) => { if (!err) { From d0ea9c83449d9dde1f2375f4e085f87963fca97f Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Wed, 4 Aug 2021 22:13:43 +0800 Subject: [PATCH 32/50] ignore the data integrity issue --- scripts/withdrawn-migration/backup.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/withdrawn-migration/backup.js b/scripts/withdrawn-migration/backup.js index 60eb0262..c737e143 100644 --- a/scripts/withdrawn-migration/backup.js +++ b/scripts/withdrawn-migration/backup.js @@ -26,7 +26,8 @@ async function backup () { try { job = await Job.findById(jc.jobId) } catch (error) { - // ignore the error + // log the error + logger.info({ component: currentStep, message: `==> Data integrity issue: Can't find the Job with Id ${jc.jobId}` }) } if (!job) continue let rb = null @@ -38,7 +39,8 @@ async function backup () { } }) } catch (error) { - // ignore the error + // log the error + logger.info({ component: currentStep, message: `==> Data integrity issue: Can't find the ResourceBooking whose userId is ${jc.userId} and jobId is ${jc.jobId}` }) } if (!rb) continue let completed = false From ca68c6a520740bbccb592d8f80f66c59cb2538a1 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 4 Aug 2021 17:16:37 +0300 Subject: [PATCH 33/50] improve docs/naming for the Notification Template rendering script --- ...son => notifications-email-demo-data.json} | 0 ...html => notifications-email-template.html} | 0 package.json | 1 - .../README.md | 22 +++++++++++++++++++ .../index.js | 13 ++++++++--- 5 files changed, 32 insertions(+), 4 deletions(-) rename data/{notifications.json => notifications-email-demo-data.json} (100%) rename data/{template.html => notifications-email-template.html} (100%) create mode 100644 scripts/notification-email-template-renderer/README.md rename scripts/{notification-renderer => notification-email-template-renderer}/index.js (56%) diff --git a/data/notifications.json b/data/notifications-email-demo-data.json similarity index 100% rename from data/notifications.json rename to data/notifications-email-demo-data.json diff --git a/data/template.html b/data/notifications-email-template.html similarity index 100% rename from data/template.html rename to data/notifications-email-template.html diff --git a/package.json b/package.json index a66131d6..a1fe6860 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "index:roles": "node scripts/es/reIndexRoles.js", "data:export": "node scripts/data/exportData.js", "data:import": "node scripts/data/importData.js", - "renderTemplate": "node scripts/notification-renderer/index.js", "migrate": "npx sequelize db:migrate", "migrate:undo": "npx sequelize db:migrate:undo", "test": "mocha test/unit/*.test.js --timeout 30000 --require test/prepare.js --exit", diff --git a/scripts/notification-email-template-renderer/README.md b/scripts/notification-email-template-renderer/README.md new file mode 100644 index 00000000..19819690 --- /dev/null +++ b/scripts/notification-email-template-renderer/README.md @@ -0,0 +1,22 @@ +# Render Email Notification Template with some data + +This script can render SendGrid Email Template (handlebars) `data/notifications-email-template.html` using some data from `data/notifications-email-demo-data.json` into `out/notifications-email-template-with-data.html`. + +## Usage + +Please run + +``` +node scripts/notification-email-template-renderer +``` + +where `` can be one of the keys in `data/notifications-email-demo-data.json` i.e: + +- `candidatesAvailableForReview` +- `interviewComingUpForHost` +- `interviewComingUpForGuest` +- `interviewCompleted` +- `postInterviewCandidateAction` +- `upcomingResourceBookingExpiration` + +The resulting file would be placed into `out/notifications-email-template-with-data.html` \ No newline at end of file diff --git a/scripts/notification-renderer/index.js b/scripts/notification-email-template-renderer/index.js similarity index 56% rename from scripts/notification-renderer/index.js rename to scripts/notification-email-template-renderer/index.js index f3c1193b..8a95b772 100644 --- a/scripts/notification-renderer/index.js +++ b/scripts/notification-email-template-renderer/index.js @@ -12,7 +12,7 @@ function render (filename, data) { return output } -const data = JSON.parse(fs.readFileSync(path.join(__dirname, '../../data/notifications.json'), 'utf8')) +const data = JSON.parse(fs.readFileSync(path.join(__dirname, '../../data/notifications-email-demo-data.json'), 'utf8')) const key = process.argv.length >= 3 ? process.argv[2] : 'candidatesAvailableForReview' @@ -21,5 +21,12 @@ if (!data[key]) { process.exit(1) } -const result = render(path.join(__dirname, '../../data/template.html'), data[key]) -fs.writeFileSync(path.join(__dirname, 'rendered.html'), result) +const outputDir = path.join(__dirname, '../../out') +const outputFile = path.join(__dirname, '../../out/notifications-email-template-with-data.html') +const result = render(path.join(__dirname, '../../data/notifications-email-template.html'), data[key]) +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir) +} +fs.writeFileSync(outputFile, result) + +console.log(`Template has been rendered to: ${outputFile}`) From 56bdf9291f7965bb064743a4c31ce7faef980a5b Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 4 Aug 2021 17:32:41 +0300 Subject: [PATCH 34/50] demo script for email notifications --- scripts/demo-email-notifications/README.md | 37 +++++++++ scripts/demo-email-notifications/index.js | 91 ++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 scripts/demo-email-notifications/README.md create mode 100644 scripts/demo-email-notifications/index.js diff --git a/scripts/demo-email-notifications/README.md b/scripts/demo-email-notifications/README.md new file mode 100644 index 00000000..c9a802d9 --- /dev/null +++ b/scripts/demo-email-notifications/README.md @@ -0,0 +1,37 @@ +# Trigger and render demo Email Notifications. + +This script does 2 things: + +- update demo data created by `npm run local:init` inside the DB in such a way that it would create situation for Email Notifications which would be triggered by the scheduler to demonstrate all possible cases. +- start Kafka Consumer that would listen to the Kafka Topic `config.NOTIFICATIONS_CREATE_TOPIC` and if there is email notification created, it would render it using provided email template `data/notifications-email-template.html` into `out` folder. + +## Usage + +1. Config scheduler to run more often so we don't have to wait to long for triggering notification, like every minute: + + ```sh + CRON_CANDIDATE_REVIEW=0 */1 * * * * + CRON_INTERVIEW_COMING_UP=0 */1 * * * * + CRON_INTERVIEW_COMPLETED=0 */1 * * * * + CRON_POST_INTERVIEW=0 */1 * * * * + CRON_UPCOMING_RESOURCE_BOOKING=0 */1 * * * * + ``` + +2. Recreate demo data by: + + ```sh + npm run local:init` + +3. Run TaaS API by: + + ```sh + npm run dev + ``` + +4. Run this demo script: + + ```sh + node scripts/demo-email-notifications + ``` + +Check the rendered emails inside `out` folder. \ No newline at end of file diff --git a/scripts/demo-email-notifications/index.js b/scripts/demo-email-notifications/index.js new file mode 100644 index 00000000..6a508f2b --- /dev/null +++ b/scripts/demo-email-notifications/index.js @@ -0,0 +1,91 @@ +const Kafka = require('no-kafka') +const fs = require('fs') +const config = require('config') +const moment = require('moment') +const handlebars = require('handlebars') +const logger = require('../../src/common/logger') +const { Interview, JobCandidate, ResourceBooking } = require('../../src/models') +const { Interviews } = require('../../app-constants') + +const consumer = new Kafka.GroupConsumer({ connectionString: process.env.KAFKA_URL, groupId: 'test-render-email' }) + +const localLogger = { + debug: message => logger.debug({ component: 'render email content', context: 'test', message }), + info: message => logger.info({ component: 'render email content', context: 'test', message }) +} + +const template = handlebars.compile(fs.readFileSync('./data/notifications-email-template.html', 'utf8')) + +/** + * Reset notification records + */ +async function resetNotificationRecords () { + // reset coming up interview records + localLogger.info('reset coming up interview records') + const interview = await Interview.findById('976d23a9-5710-453f-99d9-f57a588bb610') + const startTimestamp = moment().add(moment.duration(`PT1H`)).add('PT1M').toDate() + await interview.update({ startTimestamp, duration: 30, status: Interviews.Status.Scheduled, guestNames: ['test1', 'test2'], hostName: 'hostName' }) + + // reset completed interview records + localLogger.info('reset completed interview records') + const pastTime = moment.duration('PT1H') + const endTimestamp = moment().subtract(pastTime).toDate() + const completedInterview = await Interview.findById('9efd72c3-1dc7-4ce2-9869-8cca81d0adeb') + const duration = 30 + const completedStartTimestamp = moment().subtract(pastTime).subtract(30, 'm').toDate() + await completedInterview.update({ startTimestamp: completedStartTimestamp, duration, endTimestamp, status: Interviews.Status.Scheduled, guestNames: ['guest1', 'guest2'], hostName: 'hostName' }) + + // reset post interview candidate action reminder records + localLogger.info('reset post interview candidate action reminder records') + const jobCandidate = await JobCandidate.findById('881a19de-2b0c-4bb9-b36a-4cb5e223bdb5') + await jobCandidate.update({ status: 'interview' }) + const c2Interview = await Interview.findById('077aa2ca-5b60-4ad9-a965-1b37e08a5046') + await c2Interview.update({ startTimestamp: completedStartTimestamp, duration, endTimestamp, guestNames: ['guest1', 'guest2'], hostName: 'hostName' }) + + // reset upcoming resource booking expiration records + localLogger.info('reset upcoming resource booking expiration records') + const resourceBooking = await ResourceBooking.findById('62c3f0c9-2bf0-4f24-8647-2c802a39cbcb') + await resourceBooking.update({ endDate: moment().add(1, 'weeks').toDate() }) +} + +/** + * Init consumer. + */ +async function initConsumer () { + await consumer + .init([{ + subscriptions: [config.NOTIFICATIONS_CREATE_TOPIC], + handler: async (messageSet, topic, partition) => { + localLogger.debug(`Consumer handler. Topic: ${topic}, partition: ${partition}, message set length: ${messageSet.length}`) + for (const m of messageSet) { + const message = JSON.parse(m.message.value.toString('utf8')) + if (!fs.existsSync('out')) { + fs.mkdirSync('out') + } + if (message.payload.notifications) { + message.payload.notifications.forEach((notification) => { + const email = template(notification.details.data) + fs.writeFileSync(`./out/${notification.details.data.subject}-${Date.now()}.html`, email) + }) + } + } + } + }]) + .then(() => { + localLogger.info('Initialized.......') + localLogger.info([config.NOTIFICATIONS_CREATE_TOPIC]) + localLogger.info('Kick Start.......') + }).catch(err => { + logger.logFullError(err, { component: 'app' }) + }) +} + +/** + * Main function + */ +async function main () { + await resetNotificationRecords() + await initConsumer() +} + +main() From 0c543b88aef3d1b67a6ba076482a5c06f84b3221 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 4 Aug 2021 17:36:17 +0300 Subject: [PATCH 35/50] remove redundant code --- .gitignore | 3 --- README.md | 1 - 2 files changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 56752e55..bed42869 100644 --- a/.gitignore +++ b/.gitignore @@ -125,6 +125,3 @@ api.env # macOS files .DS_Store - -# rendered html for email template -scripts/notification-renderer/rendered.html diff --git a/README.md b/README.md index 05b1942e..9c32d290 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,6 @@ To be able to change and test `taas-es-processor` locally you can follow the nex | `npm run delete-index` | Delete Elasticsearch indexes. Use `-- --force` flag to skip confirmation | | `npm run data:import ` | Imports data into ES and db from filePath (`./data/demo-data.json` is used as default). Use `-- --force` flag to skip confirmation | | `npm run data:export ` | Exports data from ES and db into filePath (`./data/demo-data.json` is used as default). Use `-- --force` flag to skip confirmation | -| `npm run renderTemplate ` | Generates `scripts/notification-renderer/rendered.html` which has the rendered email template for the given `notificationId` where `notificationId` is one of the keys in `data/notifications.json` ex: `npm run renderTemplate upcomingResourceBookingExpiration` | | `npm run index:all` | Indexes all data from db into ES. Use `-- --force` flag to skip confirmation | | `npm run index:jobs ` | Indexes job data from db into ES, if jobId is not given all data is indexed. Use `-- --force` flag to skip confirmation | | `npm run index:job-candidates ` | Indexes job candidate data from db into ES, if jobCandidateId is not given all data is indexed. Use `-- --force` flag to skip confirmation | From fa93c02e09160da0ff86419593a1471ef50a5f90 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 4 Aug 2021 17:41:15 +0300 Subject: [PATCH 36/50] improve comments --- config/email_template.config.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/config/email_template.config.js b/config/email_template.config.js index f356e622..c76bd482 100644 --- a/config/email_template.config.js +++ b/config/email_template.config.js @@ -6,13 +6,16 @@ const config = require('config') module.exports = { - /* Report a general issue for a team. - * - * - projectId: the project ID. Example: 123412 - * - projectName: the project name. Example: "TaaS API Misc Updates" - * - reportText: the body of reported issue. Example: "I have issue with ... \n ... Thank you in advance!" + /** + * List all the kind of emails which could be sent by the endpoint `POST /taas-teams/email` inside `teamTemplates`. */ teamTemplates: { + /* Report a general issue for a team. + * + * - projectId: the project ID. Example: 123412 + * - projectName: the project name. Example: "TaaS API Misc Updates" + * - reportText: the body of reported issue. Example: "I have issue with ... \n ... Thank you in advance!" + */ 'team-issue-report': { subject: 'Issue Reported on TaaS Team {{projectName}} ({{projectId}}).', body: 'Project Name: {{projectName}}' + '\n' + @@ -103,6 +106,10 @@ module.exports = { sendgridTemplateId: config.INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID } }, + + /** + * List all kind of emails which could be send as Email Notifications by scheduler, API endpoints or anything else. + */ notificationEmailTemplates: { 'taas.notification.candidates-available-for-review': { subject: 'Topcoder - {{teamName}} has job candidates available for review', From 045af53a4c54038562d3ae216ef87721a3e8e647 Mon Sep 17 00:00:00 2001 From: yoution Date: Thu, 5 Aug 2021 00:14:36 +0800 Subject: [PATCH 37/50] fix: taas issue-412 --- docs/swagger.yaml | 12 ++++++++++++ src/services/TeamService.js | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 355b72b4..7397bca2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -5624,6 +5624,18 @@ components: isExternalMember: type: boolean description: "Is the user external member" + matchedSkills: + type: array + items: + type: string + example: "java" + description: "skills match with the role" + unMatchedSkills: + type: array + items: + type: string + example: "javascript" + description: "skills unmatch with the role" skillsMatch: type: number format: float diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 3d1f390b..22ebab19 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -803,8 +803,11 @@ async function getRoleBySkills (skills) { const roles = await Role.findAll(queryCriteria) if (roles.length > 0) { let result = _.each(roles, role => { + // role matched skills list + role.matchedSkills = _.intersection(role.listOfSkills, skills) + role.unMatchedSkills = _.difference(skills, role.matchedSkills) // calculate each found roles matching rate - role.skillsMatch = _.intersection(role.listOfSkills, skills).length / skills.length + role.skillsMatch = role.matchedSkills.length / skills.length // each role can have multiple rates, get the maximum of global rates role.maxGlobal = _.maxBy(role.rates, 'global').global }) From fc86113e5d5419aaea342eb736166e56e1bc207d Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Thu, 5 Aug 2021 14:55:42 +0800 Subject: [PATCH 38/50] migration scripts updated --- scripts/withdrawn-migration/backup.js | 14 +++++++--- scripts/withdrawn-migration/migration.js | 35 +++++++++++++++--------- scripts/withdrawn-migration/restore.js | 35 +++++++++++++++--------- 3 files changed, 54 insertions(+), 30 deletions(-) diff --git a/scripts/withdrawn-migration/backup.js b/scripts/withdrawn-migration/backup.js index c737e143..927bc4c8 100644 --- a/scripts/withdrawn-migration/backup.js +++ b/scripts/withdrawn-migration/backup.js @@ -13,13 +13,17 @@ const currentStep = 'Backup' async function backup () { logger.info({ component: currentStep, message: '*************************** Backup process started ***************************' }) const filePath = path.join(__dirname, '/temp/') + if (fs.existsSync(filePath)) { + fs.rmdirSync(filePath, { recursive: true }) + fs.mkdirSync(filePath) + } const Op = Sequelize.Op const jobCandidates = await JobCandidate.findAll({ where: { status: 'placed' } }) - + let summary = 0 for (let i = 0; i < jobCandidates.length; i++) { const jc = jobCandidates[i] let job = null @@ -56,12 +60,12 @@ async function backup () { where: filter }) if (candidates && candidates.length > 0) { - fs.writeFile(filePath + 'jobcandidate-backup.json', JSON.stringify( + summary += candidates.length + fs.writeFile(filePath + `jobcandidate-backup-${jc.userId}.json`, JSON.stringify( candidates ), (err) => { if (!err) { - logger.info({ component: `${currentStep} Summary`, message: `Backup up finished. There are ${candidates.length} jobCandidates that need to be updated` }) - logger.info({ component: currentStep, message: '*************************** Backup process finished ***************************' }) + logger.info({ component: `${currentStep} Sub`, message: `There are ${candidates.length} jobCandidates that need to be updated for userId: ${jc.userId}` }) return } logger.error({ component: currentStep, message: err.message }) @@ -70,6 +74,8 @@ async function backup () { } } } + logger.info({ component: `${currentStep}`, message: `Report: there are ${summary} jobCandidates in total` }) + logger.info({ component: currentStep, message: '*************************** Backup process finished ***************************' }) } backup().then(() => { diff --git a/scripts/withdrawn-migration/migration.js b/scripts/withdrawn-migration/migration.js index ff923e6f..b9c90ed0 100644 --- a/scripts/withdrawn-migration/migration.js +++ b/scripts/withdrawn-migration/migration.js @@ -12,19 +12,28 @@ const currentStep = 'Migration' async function migration () { logger.info({ component: currentStep, message: '*************************** Migration process started ***************************' }) const filePath = path.join(__dirname, '/temp/') - const data = fs.readFileSync(filePath + 'jobCandidate-backup.json', 'utf-8') - const jobCandidates = JSON.parse(data) - let summary = 0 - for (var i = 0; i < jobCandidates.length; i++) { - const jc = await JobCandidate.findById(jobCandidates[i].id) - if (jc) { - const oldStatus = jc.status - const updated = await jc.update({ status: config.WITHDRAWN_STATUS_CHANGE_MAPPING[jobCandidates[i].status] }) - summary++ - logger.info({ component: currentStep, message: `jobCandidate with ${jc.id} status changed from ${oldStatus} to ${updated.status}` }) - } - }; - logger.info({ component: currentStep, message: `Totally updated ${summary} jobCandidates` }) + const files = [] + fs.readdirSync(filePath).forEach(async (file) => { + files.push(`${filePath}${file}`) + }) + let totalSum = 0 + for (let j = 0; j < files.length; j++) { + const data = fs.readFileSync(files[j], 'utf-8') + const jobCandidates = JSON.parse(data) + let summary = 0 + for (let i = 0; i < jobCandidates.length; i++) { + const jc = await JobCandidate.findById(jobCandidates[i].id) + if (jc) { + const oldStatus = jc.status + const updated = await jc.update({ status: config.WITHDRAWN_STATUS_CHANGE_MAPPING[jobCandidates[i].status] }) + summary++ + totalSum++ + logger.info({ component: currentStep, message: `jobCandidate with ${jc.id} status changed from ${oldStatus} to ${updated.status}` }) + } + }; + logger.info({ component: `${currentStep} Sub`, message: `Updated ${summary} jobCandidates from ${files[j]}` }) + } + logger.info({ component: currentStep, message: `Report: Totally Updated ${totalSum} jobCandidates` }) logger.info({ component: currentStep, message: '*************************** Migration process finished ***************************' }) } diff --git a/scripts/withdrawn-migration/restore.js b/scripts/withdrawn-migration/restore.js index 0150d75a..0f842a9f 100644 --- a/scripts/withdrawn-migration/restore.js +++ b/scripts/withdrawn-migration/restore.js @@ -11,19 +11,28 @@ const currentStep = 'Restore' async function restore () { logger.info({ component: currentStep, message: '*************************** Restore process started ***************************' }) const filePath = path.join(__dirname, '/temp/') - const data = fs.readFileSync(filePath + 'jobCandidate-backup.json', 'utf-8') - const jobCandidates = JSON.parse(data) - let summary = 0 - for (var i = 0; i < jobCandidates.length; i++) { - const jc = await JobCandidate.findById(jobCandidates[i].id) - if (jc) { - const oldStatus = jc.status - const updated = await jc.update({ status: jobCandidates[i].status }) - summary++ - logger.info({ component: currentStep, message: `jobCandidate with ${jc.id} status restored from ${oldStatus} to ${updated.status}` }) - } - }; - logger.info({ component: currentStep, message: `Totally restored ${summary} jobCandidates` }) + const files = [] + fs.readdirSync(filePath).forEach(async (file) => { + files.push(`${filePath}${file}`) + }) + let totalSum = 0 + for (let j = 0; j < files.length; j++) { + const data = fs.readFileSync(files[j], 'utf-8') + const jobCandidates = JSON.parse(data) + let summary = 0 + for (var i = 0; i < jobCandidates.length; i++) { + const jc = await JobCandidate.findById(jobCandidates[i].id) + if (jc) { + const oldStatus = jc.status + const updated = await jc.update({ status: jobCandidates[i].status }) + summary++ + totalSum++ + logger.info({ component: currentStep, message: `jobCandidate with ${jc.id} status restored from ${oldStatus} to ${updated.status}` }) + } + }; + logger.info({ component: `${currentStep} Sub`, message: `Restored ${summary} jobCandidates from ${files[j]}` }) + } + logger.info({ component: currentStep, message: `Report: Totally restored ${totalSum} jobCandidates` }) logger.info({ component: currentStep, message: '*************************** Restore process finished ***************************' }) } From 51bf20e40978d5080aae78cd1a0ec042bc54115c Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Thu, 5 Aug 2021 15:13:07 +0800 Subject: [PATCH 39/50] folder creation --- config/default.js | 2 +- scripts/withdrawn-migration/backup.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/default.js b/config/default.js index b2a51bea..7f20c41d 100644 --- a/config/default.js +++ b/config/default.js @@ -189,7 +189,7 @@ module.exports = { SURVEY_COLLECTOR_PREFIX: process.env.WEEKLY_SURVEY_SURVEY_COLLECTOR_PREFIX || 'Week ending', SURVEY_MASTER_COLLECTOR_ID: process.env.WEEKLY_SURVEY_SURVEY_MASTER_COLLECTOR_ID || '', SURVEY_MASTER_MESSAGE_ID: process.env.WEEKLY_SURVEY_SURVEY_MASTER_MESSAGE_ID || '', - SURVEY_CONTACT_GROUP_ID: process.env.WEEKLY_SURVEY_SURVEY_CONTACT_GROUP_ID || '', + SURVEY_CONTACT_GROUP_ID: process.env.WEEKLY_SURVEY_SURVEY_CONTACT_GROUP_ID || '' }, // payment scheduler config PAYMENT_PROCESSING: { diff --git a/scripts/withdrawn-migration/backup.js b/scripts/withdrawn-migration/backup.js index 927bc4c8..70eb86e9 100644 --- a/scripts/withdrawn-migration/backup.js +++ b/scripts/withdrawn-migration/backup.js @@ -15,8 +15,8 @@ async function backup () { const filePath = path.join(__dirname, '/temp/') if (fs.existsSync(filePath)) { fs.rmdirSync(filePath, { recursive: true }) - fs.mkdirSync(filePath) } + fs.mkdirSync(filePath) const Op = Sequelize.Op const jobCandidates = await JobCandidate.findAll({ where: { From 8876cbb798b31809b69f018fa8fece00779d5698 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Thu, 5 Aug 2021 15:18:44 +0800 Subject: [PATCH 40/50] change naming conversion --- scripts/withdrawn-migration/backup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/withdrawn-migration/backup.js b/scripts/withdrawn-migration/backup.js index 70eb86e9..3b3ea413 100644 --- a/scripts/withdrawn-migration/backup.js +++ b/scripts/withdrawn-migration/backup.js @@ -61,7 +61,7 @@ async function backup () { }) if (candidates && candidates.length > 0) { summary += candidates.length - fs.writeFile(filePath + `jobcandidate-backup-${jc.userId}.json`, JSON.stringify( + fs.writeFile(filePath + `jobcandidate-backup-${i+1}.json`, JSON.stringify( candidates ), (err) => { if (!err) { From 80a2af937e9321cb688188f8694341969fd5ba6e Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Thu, 5 Aug 2021 10:36:03 +0300 Subject: [PATCH 41/50] sentSurvey should be `false` by default ref issue #395 --- src/services/WorkPeriodService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index 2e0b28d1..e48c6cb1 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -241,7 +241,7 @@ createWorkPeriod.schema = Joi.object().keys({ resourceBookingId: Joi.string().uuid().required(), startDate: Joi.workPeriodStartDate(), endDate: Joi.workPeriodEndDate(), - sentSurvey: Joi.boolean().default(true), + sentSurvey: Joi.boolean().default(false), daysWorked: Joi.number().integer().min(0).max(5).required(), daysPaid: Joi.number().default(0).forbidden(), paymentTotal: Joi.number().default(0).forbidden(), From e480323e613b473f6086e055a6bca166b99812ed Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Thu, 5 Aug 2021 11:28:50 +0300 Subject: [PATCH 42/50] fix updating RB.sendWeeklySurvey --- src/services/ResourceBookingService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 23ab5811..6b155ce6 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -418,7 +418,7 @@ partiallyUpdateResourceBooking.schema = Joi.object().keys({ memberRate: Joi.number().allow(null), customerRate: Joi.number().allow(null), rateType: Joi.rateType(), - sendWeeklySurvey: Joi.boolean().allow(null), + sendWeeklySurvey: Joi.boolean(), billingAccountId: Joi.number().allow(null) }).required() }).required() @@ -458,7 +458,7 @@ fullyUpdateResourceBooking.schema = Joi.object().keys({ customerRate: Joi.number().allow(null).default(null), rateType: Joi.rateType().required(), status: Joi.resourceBookingStatus().required(), - sendWeeklySurvey: Joi.boolean().allow(null), + sendWeeklySurvey: Joi.boolean().default(true), billingAccountId: Joi.number().allow(null).default(null) }).required() }).required() From 51190a3d537d3a2d177f2460caafbd898a7124b1 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Thu, 5 Aug 2021 21:43:46 +0800 Subject: [PATCH 43/50] avoid-duplicate --- data/demo-data.json | 10 +++++----- scripts/withdrawn-migration/backup.js | 20 +++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/data/demo-data.json b/data/demo-data.json index 187f763a..03d62844 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -86,7 +86,7 @@ "isApplicationPageActive": false, "minSalary": 100, "maxSalary": 200, - "hoursPerWeek": 20, + "hoursPerWeek": 90, "jobLocation": "Any location", "jobTimezone": "GMT", "currency": "USD", @@ -833,9 +833,9 @@ }, { "id": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", - "jobId": "728ff056-63f6-4730-8a9f-3074acad8479", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", - "status": "open", + "status": "placed", "externalId": "300234321", "resume": "http://example.com", "remark": "excellent", @@ -1645,11 +1645,11 @@ { "id": "c0a12936-77ef-46fa-8c75-6996339e79f6", "projectId": 111, - "userId": "05e988b7-7d54-4c10-ada1-1a04870a88a8", "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", "status": "placed", "startDate": "2020-09-27", - "endDate": "2020-10-27", + "endDate": "2021-10-27", "memberRate": 13.23, "customerRate": 13, "rateType": "hourly", diff --git a/scripts/withdrawn-migration/backup.js b/scripts/withdrawn-migration/backup.js index 3b3ea413..33cedc7f 100644 --- a/scripts/withdrawn-migration/backup.js +++ b/scripts/withdrawn-migration/backup.js @@ -24,8 +24,10 @@ async function backup () { } }) let summary = 0 + const processMapping = {} for (let i = 0; i < jobCandidates.length; i++) { const jc = jobCandidates[i] + if (processMapping[jc.userId]) continue let job = null try { job = await Job.findById(jc.jobId) @@ -60,17 +62,17 @@ async function backup () { where: filter }) if (candidates && candidates.length > 0) { - summary += candidates.length - fs.writeFile(filePath + `jobcandidate-backup-${i+1}.json`, JSON.stringify( - candidates - ), (err) => { - if (!err) { - logger.info({ component: `${currentStep} Sub`, message: `There are ${candidates.length} jobCandidates that need to be updated for userId: ${jc.userId}` }) - return - } + try { + fs.writeFileSync(filePath + `jobcandidate-backup-${i + 1}.json`, JSON.stringify( + candidates + )) + logger.info({ component: `${currentStep} Sub`, message: `There are ${candidates.length} jobCandidates that need to be updated for userId: ${jc.userId}` }) + summary += candidates.length + processMapping[jc.userId] = true + } catch (err) { logger.error({ component: currentStep, message: err.message }) process.exit(1) - }) + } } } } From 6455ccecc8d54a48aff98be0824bcf82e649d4d7 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Thu, 5 Aug 2021 21:47:17 +0800 Subject: [PATCH 44/50] fix lint --- scripts/demo-email-notifications/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/demo-email-notifications/index.js b/scripts/demo-email-notifications/index.js index 6a508f2b..7c19d2f3 100644 --- a/scripts/demo-email-notifications/index.js +++ b/scripts/demo-email-notifications/index.js @@ -23,7 +23,7 @@ async function resetNotificationRecords () { // reset coming up interview records localLogger.info('reset coming up interview records') const interview = await Interview.findById('976d23a9-5710-453f-99d9-f57a588bb610') - const startTimestamp = moment().add(moment.duration(`PT1H`)).add('PT1M').toDate() + const startTimestamp = moment().add(moment.duration('PT1H')).add('PT1M').toDate() await interview.update({ startTimestamp, duration: 30, status: Interviews.Status.Scheduled, guestNames: ['test1', 'test2'], hostName: 'hostName' }) // reset completed interview records From 9ee4dab0a971fb98d0467b0d5531ec6a1e54d358 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 6 Aug 2021 16:20:33 +0300 Subject: [PATCH 45/50] fix universal email notification kafka payload --- src/services/EmailNotificationService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/EmailNotificationService.js b/src/services/EmailNotificationService.js index f3b9bad7..f8bef90f 100644 --- a/src/services/EmailNotificationService.js +++ b/src/services/EmailNotificationService.js @@ -495,7 +495,7 @@ async function sendEmail (currentUser, data) { recipients: _.map(_.uniq([...dataRecipients, ...templateRecipients]), function (r) { return { email: r } }), cc: _.map(_.uniq([...dataCC, ...templateCC]), function (r) { return { email: r } }), data: { ...data.data, ...subjectBody }, - sendgrid_template_id: template.sendgridTemplateId, + sendgridTemplateId: template.sendgridTemplateId, version: 'v3' } } From 4593901d76585654f4846bce7bfc7115c59a6c50 Mon Sep 17 00:00:00 2001 From: darkrider97 Date: Sat, 7 Aug 2021 15:15:16 +0530 Subject: [PATCH 46/50] Adding support for generic duration format --- config/default.js | 16 ++++-- src/services/EmailNotificationService.js | 65 ++++++++++-------------- 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/config/default.js b/config/default.js index 54846412..1ddb0594 100644 --- a/config/default.js +++ b/config/default.js @@ -244,10 +244,6 @@ module.exports = { NOTIFICATION_SENDER_EMAIL: process.env.NOTIFICATION_SENDER_EMAIL, // the email notification sendgrid template id NOTIFICATION_SENDGRID_TEMPLATE_ID: process.env.NOTIFICATION_SENDGRID_TEMPLATE_ID, - // hours after interview completed when we should post the notification - INTERVIEW_COMPLETED_NOTIFICATION_HOURS: process.env.INTERVIEW_COMPLETED_NOTIFICATION_HOURS || 4, - // no of weeks before expiry when we should post the notification - RESOURCE_BOOKING_EXPIRY_NOTIFICATION_WEEKS: process.env.RESOURCE_BOOKING_EXPIRY_NOTIFICATION_WEEKS || 3, // frequency of cron checking for available candidates for review CRON_CANDIDATE_REVIEW: process.env.CRON_CANDIDATE_REVIEW || '00 00 13 * * 0-6', // frequency of cron checking for coming up interviews @@ -259,5 +255,15 @@ module.exports = { // frequency of cron checking for post interview actions CRON_POST_INTERVIEW: process.env.CRON_POST_INTERVIEW || '00 00 13 * * 0-6', // frequency of cron checking for upcoming resource bookings - CRON_UPCOMING_RESOURCE_BOOKING: process.env.CRON_UPCOMING_RESOURCE_BOOKING || '00 00 13 * * 1' + CRON_UPCOMING_RESOURCE_BOOKING: process.env.CRON_UPCOMING_RESOURCE_BOOKING || '00 00 13 * * 1', + // The match window for fetching interviews which are coming up + INTERVIEW_COMING_UP_MATCH_WINDOW: process.env.INTERVIEW_COMING_UP_MATCH_WINDOW || 'PT5M', + // The remind time for fetching interviews which are coming up + INTERVIEW_COMING_UP_REMIND_TIME: (process.env.INTERVIEW_COMING_UP_REMIND_TIME || 'PT1H,PT24H').split(','), + // The match window for fetching completed interviews + INTERVIEW_COMPLETED_MATCH_WINDOW: process.env.INTERVIEW_COMPLETED_MATCH_WINDOW || 'PT5M', + // The interview completed past time for fetching interviews + INTERVIEW_COMPLETED_PAST_TIME: process.env.INTERVIEW_COMPLETED_PAST_TIME || 'PT4H', + // The time before resource booking expiry when we should start sending notifications + RESOURCE_BOOKING_EXPIRY_TIME: process.env.RESOURCE_BOOKING_EXPIRY_TIME || 'P21D' } diff --git a/src/services/EmailNotificationService.js b/src/services/EmailNotificationService.js index f8bef90f..fbc86152 100644 --- a/src/services/EmailNotificationService.js +++ b/src/services/EmailNotificationService.js @@ -88,7 +88,7 @@ async function getDataForInterview (interview, jobCandidate, job) { const interviewLink = `${config.TAAS_APP_URL}/${job.projectId}/positions/${job.id}/candidates/interviews` const guestName = _.isEmpty(interview.guestNames) ? '' : interview.guestNames[0] - const startTime = _.isEmpty(interview.startTimestamp) ? '' : interview.startTimestamp.toUTCString() + const startTime = interview.startTimestamp ? interview.startTimestamp.toUTCString() : '' return { jobTitle: job.title, @@ -182,43 +182,33 @@ async function sendCandidatesAvailableEmails () { */ async function sendInterviewComingUpEmails () { const currentTime = moment.utc() - const minutesRange = 5 - - const oneDayFromNow = currentTime.clone().add(24, 'hours') - const dayEndTime = oneDayFromNow.clone().add(minutesRange, 'minutes') + const timestampFilter = { + [Op.or]: [] + } + const window = moment.duration(config.INTERVIEW_COMING_UP_MATCH_WINDOW) + for (const remindTime of config.INTERVIEW_COMING_UP_REMIND_TIME) { + const rangeStart = currentTime.clone().add(moment.duration(remindTime)) + const rangeEnd = rangeStart.clone().add(window) + + timestampFilter[Op.or].push({ + [Op.and]: [ + { + [Op.gt]: rangeStart + }, + { + [Op.lte]: rangeEnd + } + ] + }) + } - const oneHourFromNow = currentTime.clone().add(1, 'hour') - const hourEndTime = oneHourFromNow.clone().add(minutesRange, 'minutes') const filter = { [Op.and]: [ { status: { [Op.eq]: constants.Interviews.Status.Scheduled } }, { - startTimestamp: { - [Op.or]: [ - { - [Op.and]: [ - { - [Op.gt]: oneDayFromNow - }, - { - [Op.lte]: dayEndTime - } - ] - }, - { - [Op.and]: [ - { - [Op.gt]: oneHourFromNow - }, - { - [Op.lte]: hourEndTime - } - ] - } - ] - } + startTimestamp: timestampFilter } ] } @@ -272,9 +262,9 @@ async function sendInterviewComingUpEmails () { * Sends email reminder to the interview host after it ends to change the interview status */ async function sendInterviewCompletedEmails () { - const minutesRange = 5 - const hoursBeforeNow = moment.utc().subtract(config.INTERVIEW_COMPLETED_NOTIFICATION_HOURS, 'hours') - const endTime = hoursBeforeNow.clone().add(minutesRange, 'minutes') + const window = moment.duration(config.INTERVIEW_COMPLETED_MATCH_WINDOW) + const rangeStart = moment.utc().subtract(moment.duration(config.INTERVIEW_COMPLETED_PAST_TIME)) + const rangeEnd = rangeStart.clone().add(window) const filter = { [Op.and]: [ { @@ -284,10 +274,10 @@ async function sendInterviewCompletedEmails () { endTimestamp: { [Op.and]: [ { - [Op.gte]: hoursBeforeNow + [Op.gte]: rangeStart }, { - [Op.lt]: endTime + [Op.lt]: rangeEnd } ] } @@ -396,7 +386,8 @@ async function sendPostInterviewActionEmails () { */ async function sendResourceBookingExpirationEmails () { const currentTime = moment.utc() - const maxEndDate = currentTime.clone().add(config.RESOURCE_BOOKING_EXPIRY_NOTIFICATION_WEEKS, 'weeks') + const maxEndDate = currentTime.clone().add(moment.duration(config.RESOURCE_BOOKING_EXPIRY_TIME)) + const expiringResourceBookings = await ResourceBooking.findAll({ where: { endDate: { From 3d7022e58075a6bd21f43e6e0cd57dbd865b713c Mon Sep 17 00:00:00 2001 From: darkrider97 Date: Sat, 7 Aug 2021 19:01:21 +0530 Subject: [PATCH 47/50] Making sure all email notifications are coming through demo script --- data/demo-data.json | 36 +++++++++++----------- scripts/demo-email-notifications/README.md | 4 ++- scripts/demo-email-notifications/index.js | 9 +++--- src/services/EmailNotificationService.js | 6 ---- 4 files changed, 26 insertions(+), 29 deletions(-) diff --git a/data/demo-data.json b/data/demo-data.json index 03d62844..0e67b097 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -859,9 +859,9 @@ "startTimestamp": null, "endTimestamp": null, "hostName": null, - "hostEmail": null, - "guestNames": null, - "guestEmails": null, + "hostEmail": "interviewhost@tc.com", + "guestNames": ["guest name1", "guest name2"], + "guestEmails": ["guest1@tc.com", "guest2@tc.com"], "status": "Completed", "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -900,9 +900,9 @@ "startTimestamp": null, "endTimestamp": null, "hostName": null, - "hostEmail": null, - "guestNames": null, - "guestEmails": null, + "hostEmail": "interviewhost@tc.com", + "guestNames": ["guest name1", "guest name2"], + "guestEmails": ["guest1@tc.com", "guest2@tc.com"], "status": "Scheduling", "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -926,9 +926,9 @@ "startTimestamp": null, "endTimestamp": null, "hostName": null, - "hostEmail": null, - "guestNames": null, - "guestEmails": null, + "hostEmail": "interviewhost@tc.com", + "guestNames": ["guest name1", "guest name2"], + "guestEmails": ["guest1@tc.com", "guest2@tc.com"], "status": "Completed", "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -967,9 +967,9 @@ "startTimestamp": null, "endTimestamp": null, "hostName": null, - "hostEmail": null, - "guestNames": null, - "guestEmails": null, + "hostEmail": "interviewhost@tc.com", + "guestNames": ["guest name1", "guest name2"], + "guestEmails": ["guest1@tc.com", "guest2@tc.com"], "status": "Scheduling", "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -993,9 +993,9 @@ "startTimestamp": null, "endTimestamp": null, "hostName": null, - "hostEmail": null, - "guestNames": null, - "guestEmails": null, + "hostEmail": "interviewhost@tc.com", + "guestNames": ["guest name1", "guest name2"], + "guestEmails": ["guest1@tc.com", "guest2@tc.com"], "status": "Scheduling", "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -1019,9 +1019,9 @@ "startTimestamp": null, "endTimestamp": null, "hostName": null, - "hostEmail": null, - "guestNames": null, - "guestEmails": null, + "hostEmail": "interviewhost@tc.com", + "guestNames": ["guest name1", "guest name2"], + "guestEmails": ["guest1@tc.com", "guest2@tc.com"], "status": "Completed", "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", diff --git a/scripts/demo-email-notifications/README.md b/scripts/demo-email-notifications/README.md index c9a802d9..3a5dbd47 100644 --- a/scripts/demo-email-notifications/README.md +++ b/scripts/demo-email-notifications/README.md @@ -15,6 +15,8 @@ This script does 2 things: CRON_INTERVIEW_COMPLETED=0 */1 * * * * CRON_POST_INTERVIEW=0 */1 * * * * CRON_UPCOMING_RESOURCE_BOOKING=0 */1 * * * * + INTERVIEW_COMING_UP_MATCH_WINDOW=PT1M + INTERVIEW_COMPLETED_MATCH_WINDOW=PT1M ``` 2. Recreate demo data by: @@ -34,4 +36,4 @@ This script does 2 things: node scripts/demo-email-notifications ``` -Check the rendered emails inside `out` folder. \ No newline at end of file +Check the rendered emails inside `out` folder. diff --git a/scripts/demo-email-notifications/index.js b/scripts/demo-email-notifications/index.js index 7c19d2f3..23f3ac9f 100644 --- a/scripts/demo-email-notifications/index.js +++ b/scripts/demo-email-notifications/index.js @@ -23,13 +23,13 @@ async function resetNotificationRecords () { // reset coming up interview records localLogger.info('reset coming up interview records') const interview = await Interview.findById('976d23a9-5710-453f-99d9-f57a588bb610') - const startTimestamp = moment().add(moment.duration('PT1H')).add('PT1M').toDate() + const startTimestamp = moment().add(moment.duration(config.INTERVIEW_COMING_UP_REMIND_TIME[0])).add(config.INTERVIEW_COMING_UP_MATCH_WINDOW).toDate() await interview.update({ startTimestamp, duration: 30, status: Interviews.Status.Scheduled, guestNames: ['test1', 'test2'], hostName: 'hostName' }) // reset completed interview records localLogger.info('reset completed interview records') - const pastTime = moment.duration('PT1H') - const endTimestamp = moment().subtract(pastTime).toDate() + const pastTime = moment.duration(config.INTERVIEW_COMPLETED_PAST_TIME) + const endTimestamp = moment().subtract(pastTime).add(config.INTERVIEW_COMPLETED_MATCH_WINDOW).toDate() const completedInterview = await Interview.findById('9efd72c3-1dc7-4ce2-9869-8cca81d0adeb') const duration = 30 const completedStartTimestamp = moment().subtract(pastTime).subtract(30, 'm').toDate() @@ -45,7 +45,8 @@ async function resetNotificationRecords () { // reset upcoming resource booking expiration records localLogger.info('reset upcoming resource booking expiration records') const resourceBooking = await ResourceBooking.findById('62c3f0c9-2bf0-4f24-8647-2c802a39cbcb') - await resourceBooking.update({ endDate: moment().add(1, 'weeks').toDate() }) + const testEnd = moment().add(moment.duration(config.RESOURCE_BOOKING_EXPIRY_TIME)).toDate() + await resourceBooking.update({ endDate: testEnd }) } /** diff --git a/src/services/EmailNotificationService.js b/src/services/EmailNotificationService.js index fbc86152..5572933c 100644 --- a/src/services/EmailNotificationService.js +++ b/src/services/EmailNotificationService.js @@ -126,8 +126,6 @@ async function sendCandidatesAvailableEmails () { if (!project) { continue } const recipientEmails = getProjectMembersEmails(project) - if (_.isEmpty(recipientEmails)) { continue } - const projectJobs = _.filter(jobs, job => job.projectId === projectId) const teamJobs = [] @@ -348,8 +346,6 @@ async function sendPostInterviewActionEmails () { if (!project) { continue } const recipientEmails = getProjectMembersEmails(project) - if (_.isEmpty(recipientEmails)) { continue } - const projectJobs = _.filter(jobs, job => job.projectId === projectId) const teamInterviews = [] let numCandidates = 0 @@ -418,8 +414,6 @@ async function sendResourceBookingExpirationEmails () { const project = await getProjectWithId(projectId) if (!project) { continue } const recipientEmails = getProjectMembersEmails(project) - if (_.isEmpty(recipientEmails)) { continue } - const projectJobs = _.filter(jobs, job => job.projectId === projectId) let numResourceBookings = 0 From 10845c4d10f73e69d8e4f0729441bcbf64d6a93b Mon Sep 17 00:00:00 2001 From: darkrider97 Date: Sun, 8 Aug 2021 17:20:11 +0530 Subject: [PATCH 48/50] Adding no-kafka package dependency as demo-email-notifications script uses it --- package-lock.json | 113 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 114 insertions(+) diff --git a/package-lock.json b/package-lock.json index dce8640e..ea69e2a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -585,6 +585,12 @@ } } }, + "@types/bluebird": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-JjNHCk6r6aR82aRf2yDtX5NAe8o=", + "dev": true + }, "@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", @@ -645,6 +651,12 @@ "@types/express": "*" } }, + "@types/lodash": { + "version": "4.14.172", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz", + "integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==", + "dev": true + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -1297,6 +1309,17 @@ "tweetnacl": "^0.14.3" } }, + "bin-protocol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bin-protocol/-/bin-protocol-3.1.1.tgz", + "integrity": "sha512-9vCGfaHC2GBHZwGQdG+DpyXfmLvx9uKtf570wMLwIc9wmTIDgsdCBXQxTZu5X2GyogkfBks2Ode4N0sUVxJ2qQ==", + "dev": true, + "requires": { + "lodash": "^4.17.11", + "long": "^4.0.0", + "protocol-buffers-schema": "^3.0.0" + } + }, "binary-extensions": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", @@ -1433,6 +1456,12 @@ "isarray": "^1.0.0" } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1836,6 +1865,12 @@ "xdg-basedir": "^4.0.0" } }, + "connection-parse": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/connection-parse/-/connection-parse-0.0.7.tgz", + "integrity": "sha1-GOcxiqsGppkmc3KxDFIm0locmmk=", + "dev": true + }, "contains-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", @@ -3301,6 +3336,16 @@ "type-fest": "^0.8.0" } }, + "hashring": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/hashring/-/hashring-3.2.0.tgz", + "integrity": "sha1-/aTv3oqiLNuX+x0qZeiEAeHBRM4=", + "dev": true, + "requires": { + "connection-parse": "0.0.x", + "simple-lru-cache": "0.0.x" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -4629,6 +4674,12 @@ } } }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dev": true + }, "long-timeout": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", @@ -4973,6 +5024,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "murmur-hash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmur-hash-js/-/murmur-hash-js-1.0.0.tgz", + "integrity": "sha1-UEEEkmnJZjPIZjhpYLL0KJ515bA=", + "dev": true + }, "mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -5047,6 +5104,15 @@ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, + "nice-simple-logger": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nice-simple-logger/-/nice-simple-logger-1.0.1.tgz", + "integrity": "sha1-D55khSe+e+PkmrdvqMjAmK+VG/Y=", + "dev": true, + "requires": { + "lodash": "^4.3.0" + } + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -5082,6 +5148,32 @@ } } }, + "no-kafka": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/no-kafka/-/no-kafka-3.4.3.tgz", + "integrity": "sha512-hYnkg1OWVdaxORdzVvdQ4ueWYpf7IICObPzd24BBiDyVG5219VkUnRxSH9wZmisFb6NpgABzlSIL1pIZaCKmXg==", + "dev": true, + "requires": { + "@types/bluebird": "3.5.0", + "@types/lodash": "^4.14.55", + "bin-protocol": "^3.1.1", + "bluebird": "^3.3.3", + "buffer-crc32": "^0.2.5", + "hashring": "^3.2.0", + "lodash": "=4.17.11", + "murmur-hash-js": "^1.0.0", + "nice-simple-logger": "^1.0.1", + "wrr-pool": "^1.0.3" + }, + "dependencies": { + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + } + } + }, "node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -6133,6 +6225,12 @@ "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "dev": true }, + "protocol-buffers-schema": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.5.1.tgz", + "integrity": "sha512-YVCvdhxWNDP8/nJDyXLuM+UFsuPk4+1PB7WGPVDzm3HTHbzFLxQYeW2iZpS4mmnXrQJGBzt230t/BbEb7PrQaw==", + "dev": true + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -6809,6 +6907,12 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, + "simple-lru-cache": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/simple-lru-cache/-/simple-lru-cache-0.0.2.tgz", + "integrity": "sha1-1ZzDoZPBpdAyD4Tucy9uRxPlEd0=", + "dev": true + }, "simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -7993,6 +8097,15 @@ "typedarray-to-buffer": "^3.1.5" } }, + "wrr-pool": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/wrr-pool/-/wrr-pool-1.1.4.tgz", + "integrity": "sha512-+lEdj42HlYqmzhvkZrx6xEymj0wzPBxqr7U1Xh9IWikMzOge03JSQT9YzTGq54SkOh/noViq32UejADZVzrgAg==", + "dev": true, + "requires": { + "lodash": "^4.17.11" + } + }, "xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", diff --git a/package.json b/package.json index a1fe6860..4262080f 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "csv-parser": "^3.0.0", "handlebars": "^4.7.7", "mocha": "^8.1.3", + "no-kafka": "^3.4.3", "nodemon": "^2.0.4", "nyc": "^15.1.0", "sequelize-cli": "^6.2.0", From 53a5cc6577d3b55cdcc586ff4657585befe6e4b0 Mon Sep 17 00:00:00 2001 From: Arpitkumar Chaudhari Date: Mon, 9 Aug 2021 02:58:27 +0530 Subject: [PATCH 49/50] Calculating Amount & Stripe APIs --- README.md | 3 + docs/swagger.yaml | 130 ++++++++++++++++++++++++++++++ package-lock.json | 9 +++ package.json | 1 + src/controllers/TeamController.js | 22 ++++- src/routes/TeamRoutes.js | 14 +++- src/services/TeamService.js | 27 +++++++ 7 files changed, 204 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9c32d290..a8215223 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,9 @@ ES_HOST=http://dockerhost:9200 DATABASE_URL=postgres://postgres:postgres@dockerhost:5432/postgres BUSAPI_URL=http://dockerhost:8002/v5 + # stripe + STRIPE_SECRET_KEY= + CURRENCY=usd ``` - Values from this file would be automatically used by many `npm` commands. diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 65d420f1..2ab802b2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3435,6 +3435,110 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /taas-teams/calculateAmount: + post: + tags: + - Teams + description: | + Calculates total amount for the team. + + **Authorization** Any Topcoder user with valid token is allowed. For not logged users Topcoder m2m token with create:taas-teams scope is allowed. + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CalculateAmountRequestBody" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/CalculateAmountResponse" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "409": + description: Conflict + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /taas-teams/createPayment: + post: + tags: + - Teams + description: | + Calculates total amount for the team. + + **Authorization** Any Topcoder user with valid token is allowed. For not logged users Topcoder m2m token with create:taas-teams scope is allowed. + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePaymentRequestBody" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePaymentResponse" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "409": + description: Conflict + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /taas-teams/members-suggest/{fragment}: get: tags: @@ -5589,6 +5693,32 @@ components: type: string description: "Optional job title." example: "Lead Application Developer" + CalculateAmountRequestBody: + properties: + numberOfResources: + type: number + description: "No. of resources required." + rates: + type: number + description: "Weekly rates" + durationWeeks: + type: number + description: "No. of weeks" + CalculateAmountResponse: + properties: + totalAmount: + type: number + description: "Total amount calculated" + CreatePaymentRequestBody: + properties: + totalAmount: + type: number + description: "Total amount charged to user via stripe" + CreatePaymentResponse: + properties: + paymentIntentToken: + type: string + description: " Token required by stripe for completing payment." SubmitTeamRequestBody: properties: teamName: diff --git a/package-lock.json b/package-lock.json index 3d174e12..56bb51a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7155,6 +7155,15 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, + "stripe": { + "version": "8.168.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.168.0.tgz", + "integrity": "sha512-MQXTarijIOagtLajGe1zBFc9KMbB7jIoFv/kr1WsDPJO/S+/hhZjsXCgBkNvnlwK7Yl0VUn+YrgXl9/9wU6WCw==", + "requires": { + "@types/node": ">=8.1.0", + "qs": "^6.6.0" + } + }, "success-symbol": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/success-symbol/-/success-symbol-0.1.0.tgz", diff --git a/package.json b/package.json index 6c47684a..4beb0c38 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "prompt-confirm": "^2.0.4", "rewire": "^5.0.0", "sequelize": "^6.3.5", + "stripe": "^8.168.0", "superagent": "^6.1.0", "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6", "util": "^0.12.3", diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index 65e5262f..e34fa943 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -155,6 +155,24 @@ async function suggestMembers (req, res) { res.send(await service.suggestMembers(req.authUser, req.params.fragment)) } +/** + * + * @param req the request + * @param res the response + */ + async function calculateAmount(req, res) { + res.send(await service.calculateAmount(req.body)); +} + +/** + * + * @param req the request + * @param res the response + */ +async function createPayment(req, res) { + res.send(await service.createPayment(req.body.totalAmount)); +} + module.exports = { searchTeams, getTeam, @@ -169,5 +187,7 @@ module.exports = { roleSearchRequest, createTeam, searchSkills, - suggestMembers + suggestMembers, + createPayment, + calculateAmount } diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index a4c1ca5e..3c83d5f2 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -107,5 +107,17 @@ module.exports = { auth: 'jwt', scopes: [] } - } + }, + "/taas-teams/calculateAmount": { + post: { + controller: "TeamController", + method: "calculateAmount", + }, + }, + "/taas-teams/createPayment": { + post: { + controller: "TeamController", + method: "createPayment", + }, +} } diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 3d1f390b..35269e8d 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -20,6 +20,7 @@ const { getAuditM2Muser } = require('../common/helper') const { matchedSkills, unMatchedSkills } = require('../../scripts/emsi-mapping/esmi-skills-mapping') const Role = models.Role const RoleSearchRequest = models.RoleSearchRequest +const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { return { @@ -1162,6 +1163,30 @@ suggestMembers.schema = Joi.object().keys({ fragment: Joi.string().required() }).required() +/** + * Calculates total amount + * @param {Object} body + * @returns {int} totalAmount + */ + async function calculateAmount(body) { + const totalAmount = body.numberOfResources * body.rates * body.durationWeeks; + return { totalAmount }; +} + +/** + * Creates token for stripe + * @param {int} totalAmount + * @returns {string} paymentIntentToken + */ +async function createPayment(totalAmount) { + const paymentIntent = await stripe.paymentIntents.create({ + amount: totalAmount, + currency: process.env.CURRENCY, + }); + return { paymentIntentToken: paymentIntent.client_secret }; +} + + module.exports = { searchTeams, getTeam, @@ -1180,6 +1205,8 @@ module.exports = { createRoleSearchRequest, isExternalMember, createTeam, + calculateAmount, + createPayment, searchSkills, suggestMembers } From 459596443fe1cea79a29d310e001989176edc80f Mon Sep 17 00:00:00 2001 From: Cagdas U Date: Mon, 9 Aug 2021 07:54:27 +0300 Subject: [PATCH 50/50] fix(role-service): update validations to accept positive integers * Update JOI validations of the below fields to accept only positive integers. * `timeToInterview`, * `timeToCandidate`, * `numberOfMembers`, * `numberOfMembersAvailable`, * all rate fields Addresses: https://github.com/topcoder-platform/taas-app/issues/403#issuecomment-892774361, https://github.com/topcoder-platform/taas-app/issues/425 --- src/bootstrap.js | 2 +- src/services/RoleService.js | 64 ++++++++++++++++++------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/bootstrap.js b/src/bootstrap.js index a81e5dcc..896b14f7 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -26,7 +26,7 @@ Joi.workPeriodPaymentUpdateStatus = () => Joi.string().valid(..._.values(WorkPer // See https://joi.dev/api/?v=17.3.0#string fro details why it's like this. // In many cases we would like to allow empty string to make it easier to create UI for editing data. Joi.stringAllowEmpty = () => Joi.string().allow('') -Joi.smallint = () => Joi.number().min(-32768).max(32767) +Joi.smallint = () => Joi.number().integer().min(-32768).max(32767) function buildServices (dir) { const files = fs.readdirSync(dir) diff --git a/src/services/RoleService.js b/src/services/RoleService.js index 7ff7de65..ba128170 100644 --- a/src/services/RoleService.js +++ b/src/services/RoleService.js @@ -131,24 +131,24 @@ createRole.schema = Joi.object().keys({ description: Joi.string().max(1000), listOfSkills: Joi.array().items(Joi.string().max(50).required()), rates: Joi.array().items(Joi.object().keys({ - global: Joi.smallint(), - inCountry: Joi.smallint(), - offShore: Joi.smallint(), - niche: Joi.smallint(), - rate30Niche: Joi.smallint(), - rate30Global: Joi.smallint(), - rate30InCountry: Joi.smallint(), - rate30OffShore: Joi.smallint(), - rate20Niche: Joi.smallint(), - rate20Global: Joi.smallint(), - rate20InCountry: Joi.smallint(), - rate20OffShore: Joi.smallint() + global: Joi.smallint().min(1), + inCountry: Joi.smallint().min(1), + offShore: Joi.smallint().min(1), + niche: Joi.smallint().min(1), + rate30Niche: Joi.smallint().min(1), + rate30Global: Joi.smallint().min(1), + rate30InCountry: Joi.smallint().min(1), + rate30OffShore: Joi.smallint().min(1), + rate20Niche: Joi.smallint().min(1), + rate20Global: Joi.smallint().min(1), + rate20InCountry: Joi.smallint().min(1), + rate20OffShore: Joi.smallint().min(1) }).required()).required(), - numberOfMembers: Joi.number(), - numberOfMembersAvailable: Joi.smallint(), + numberOfMembers: Joi.number().integer().min(1), + numberOfMembersAvailable: Joi.smallint().min(1), imageUrl: Joi.string().uri().max(255), - timeToCandidate: Joi.smallint(), - timeToInterview: Joi.smallint() + timeToCandidate: Joi.smallint().min(1), + timeToInterview: Joi.smallint().min(1) }).required() }).required() @@ -189,24 +189,24 @@ updateRole.schema = Joi.object().keys({ description: Joi.string().max(1000).allow(null), listOfSkills: Joi.array().items(Joi.string().max(50).required()).allow(null), rates: Joi.array().items(Joi.object().keys({ - global: Joi.smallint().required(), - inCountry: Joi.smallint().required(), - offShore: Joi.smallint().required(), - niche: Joi.smallint(), - rate30Niche: Joi.smallint(), - rate30Global: Joi.smallint(), - rate30InCountry: Joi.smallint(), - rate30OffShore: Joi.smallint(), - rate20Global: Joi.smallint(), - rate20Niche: Joi.smallint(), - rate20InCountry: Joi.smallint(), - rate20OffShore: Joi.smallint() + global: Joi.smallint().min(1).required(), + inCountry: Joi.smallint().min(1).required(), + offShore: Joi.smallint().min(1).required(), + niche: Joi.smallint().min(1), + rate30Niche: Joi.smallint().min(1), + rate30Global: Joi.smallint().min(1), + rate30InCountry: Joi.smallint().min(1), + rate30OffShore: Joi.smallint().min(1), + rate20Global: Joi.smallint().min(1), + rate20Niche: Joi.smallint().min(1), + rate20InCountry: Joi.smallint().min(1), + rate20OffShore: Joi.smallint().min(1) }).required()), - numberOfMembers: Joi.number().allow(null), - numberOfMembersAvailable: Joi.smallint().allow(null), + numberOfMembers: Joi.number().integer().min(1).allow(null), + numberOfMembersAvailable: Joi.smallint().min(1).allow(null), imageUrl: Joi.string().uri().max(255).allow(null), - timeToCandidate: Joi.smallint().allow(null), - timeToInterview: Joi.smallint().allow(null) + timeToCandidate: Joi.smallint().min(1).allow(null), + timeToInterview: Joi.smallint().min(1).allow(null) }).required() }).required()