From 0faa3a3d2a70e7a4254aba275386cd315a77fc5d Mon Sep 17 00:00:00 2001 From: xxcxy Date: Thu, 12 Aug 2021 22:50:57 +0800 Subject: [PATCH 1/2] imp API Triggered Notifications --- config/default.js | 6 + config/email_template.config.js | 35 ++ data/notifications-email-template.html | 96 ++++++ ...coder-bookings-api.postman_collection.json | 325 +++++++++++++++++- scripts/demo-email-notifications/index.js | 10 +- src/common/helper.js | 8 +- src/controllers/TeamController.js | 8 +- src/eventHandlers/InterviewEventHandler.js | 137 +++++++- src/eventHandlers/JobCandidateEventHandler.js | 82 +++++ src/eventHandlers/JobEventHandler.js | 53 ++- .../ResourceBookingEventHandler.js | 45 +++ src/eventHandlers/TeamEventHandler.js | 59 ++++ src/eventHandlers/index.js | 6 +- src/routes/TeamRoutes.js | 18 +- src/services/JobService.js | 7 +- src/services/TeamService.js | 26 +- 16 files changed, 885 insertions(+), 36 deletions(-) create mode 100644 src/eventHandlers/TeamEventHandler.js diff --git a/config/default.js b/config/default.js index 1a012a6d..4f31db75 100644 --- a/config/default.js +++ b/config/default.js @@ -142,6 +142,8 @@ module.exports = { TAAS_ROLE_UPDATE_TOPIC: process.env.TAAS_ROLE_UPDATE_TOPIC || 'taas.role.update', // the delete role entity Kafka message topic TAAS_ROLE_DELETE_TOPIC: process.env.TAAS_ROLE_DELETE_TOPIC || 'taas.role.delete', + // the create team entity message topic, only used for eventHandler + TAAS_TEAM_CREATE_TOPIC: process.env.TAAS_TEAM_CREATE_TOPIC || 'taas.team.create', // special kafka topics TAAS_ACTION_RETRY_TOPIC: process.env.TAAS_ACTION_RETRY_TOPIC || 'taas.action.retry', @@ -161,6 +163,10 @@ module.exports = { // INTERVIEW_INVITATION_RECIPIENTS_LIST may contain comma-separated list of email which is converted to array // scheduler@x.ai should be in the RECIPIENTS list INTERVIEW_INVITATION_RECIPIENTS_LIST: (process.env.INTERVIEW_INVITATION_RECIPIENTS_LIST || 'scheduler@topcoder.com').split(','), + // the emails address for overlapping interview + NOTIFICATION_OPS_EMAILS: (process.env.NOTIFICATION_OPS_EMAILS || 'overlapping@topcoder.com').split(','), + // the slack channel for sending notifications + NOTIFICATION_SLACK_CHANNEL: process.env.NOTIFICATION_SLACK_CHANNEL || '#dev-general', // SendGrid email template ID for reporting issue REPORT_ISSUE_SENDGRID_TEMPLATE_ID: process.env.REPORT_ISSUE_SENDGRID_TEMPLATE_ID, // SendGrid email template ID for requesting extension diff --git a/config/email_template.config.js b/config/email_template.config.js index c76bd482..06ea4fb0 100644 --- a/config/email_template.config.js +++ b/config/email_template.config.js @@ -152,6 +152,41 @@ module.exports = { recipients: [], from: config.NOTIFICATION_SENDER_EMAIL, sendgridTemplateId: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'taas.notification.team-created': { + subject: 'Topcoder - Team Created', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgridTemplateId: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'taas.notification.job-created': { + subject: 'Topcoder - Job Created', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgridTemplateId: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'taas.notification.interviews-overlapping': { + subject: 'Topcoder - Interviews overlapping', + body: '', + recipients: config.NOTIFICATION_OPS_EMAILS, + from: config.NOTIFICATION_SENDER_EMAIL, + sendgridTemplateId: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'taas.notification.job-candidate-selected': { + subject: 'Topcoder - Job Candidate Selected', + body: '', + recipients: config.NOTIFICATION_OPS_EMAILS, + from: config.NOTIFICATION_SENDER_EMAIL, + sendgridTemplateId: config.NOTIFICATION_SENDGRID_TEMPLATE_ID + }, + 'taas.notification.resource-booking-placed': { + subject: 'Topcoder - Resource Booking Placed', + body: '', + recipients: [], + from: config.NOTIFICATION_SENDER_EMAIL, + sendgridTemplateId: config.NOTIFICATION_SENDGRID_TEMPLATE_ID } } } diff --git a/data/notifications-email-template.html b/data/notifications-email-template.html index 7dcb8f30..6d263281 100644 --- a/data/notifications-email-template.html +++ b/data/notifications-email-template.html @@ -240,6 +240,102 @@ {{/each}} {{/if}} + {{#if notificationType.newTeamCreated}} + Team Name: + {{teamName}} + + + + + + + {{#each jobList}} + + + + + + {{/each}} +
Job TitleDurationStart Date
{{this.title}}{{this.duration}}{{this.startDate}}
+ {{/if}} + {{#if notificationType.newJobCreated}} + + + + + + + + + + + + + +
Team NameJob titleDurationStart Date
{{teamName}}{{jobTitle}}{{jobDuration}}{{jobStartDate}}
+ {{/if}} + {{#if notificationType.overlappingInterview}} + + + + + + + + + + + {{#each interviews}} + + + + + + + + + + {{/each}} +
Team NameTeam URLJob titleJob URLJob CandidateInterview Start DateInterview End Date
{{this.teamName}}{{this.teamURL}}{{this.jobTitle}}{{this.jobURL}}{{this.candidateUserHandle}}{{this.startTime}}{{this.endTime}}
+ {{/if}} + {{#if notificationType.candidateSelected}} + + + + + + + + + + + + + + + + + +
Team NameJob titleJob URLJob Start DateJob DurationCandidate User Handle
{{teamName}}{{jobTitle}}{{jobUrl}}{{jobStartDate}}{{jobDuration}}{{userHandle}}
+ {{/if}} + {{#if notificationType.resourceBookingPlaced}} + + + + + + + + + + + + + + + +
Team NameJob TitleResource Bookings HandleResource Bookings Start DateResource Bookings End Date
{{teamName}}{{jobTitle}}{{userHandle}}{{startDate}}{{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. diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 47e81133..e20d62bf 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "709e8fdf-f5f4-4053-a679-b89504637cc8", + "_postman_id": "99905d78-e7c8-42ee-8aca-2c7fe3f8df4a", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -33806,6 +33806,9 @@ } ], "request": { + "auth": { + "type": "noauth" + }, "method": "POST", "header": [ { @@ -34297,6 +34300,326 @@ } }, "response": [] + }, + { + "name": "[New Team created]", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"teamName\": \"New Team created notification\",\r\n \"teamDescription\":\"New Team created notification description\",\r\n \"positions\": [\r\n {\r\n \"roleName\": \"Dev Ops Engineer\",\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId1-1}}\",\r\n \"numberOfResources\": 10,\r\n \"durationWeeks\": 7,\r\n \"startMonth\": \"2021-08-15\"\r\n },\r\n {\r\n \"roleName\": \"Salesforce Developer\",\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId1-2}}\",\r\n \"numberOfResources\": 5,\r\n \"durationWeeks\": 5,\r\n \"startMonth\": \"2021-08-15\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/submitTeamRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "submitTeamRequest" + ] + } + }, + "response": [] + }, + { + "name": "[New Job created]", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var data = JSON.parse(responseBody);", + "postman.setEnvironmentVariable(\"jobId\",data.id);" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"duration\": 1,\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"workload\": \"full-time\",\r\n \"skills\": [\r\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\r\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\r\n \"cbac57a3-7180-4316-8769-73af64893158\",\r\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\r\n ],\r\n \"title\": \"Dummy title - at most 64 characters\",\r\n \"minSalary\": 100,\r\n \"maxSalary\": 200,\r\n \"hoursPerWeek\": 20,\r\n \"jobLocation\": \"Any location\",\r\n \"jobTimezone\": \"GMT\",\r\n \"currency\": \"USD\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobs", + "host": [ + "{{URL}}" + ], + "path": [ + "jobs" + ] + } + }, + "response": [] + }, + { + "name": "[Overlapping Interview Invites prepare]", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"startTimestamp\": \"2022-08-12T12:54:54.948Z\",\r\n \"endTimestamp\": \"2022-08-12T13:24:54.948Z\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobCandidates/827ee401-df04-42e1-abbe-7b97ce7937ff/updateInterview/2", + "host": [ + "{{URL}}" + ], + "path": [ + "jobCandidates", + "827ee401-df04-42e1-abbe-7b97ce7937ff", + "updateInterview", + "2" + ] + } + }, + "response": [] + }, + { + "name": "[Overlapping Interview Invites]", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"startTimestamp\": \"2022-08-12T13:00:54.948Z\",\r\n \"endTimestamp\": \"2022-08-12T13:30:54.948Z\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobCandidates/a4ea7bcf-5b99-4381-b99c-a9bd05d83a36/updateInterview/3", + "host": [ + "{{URL}}" + ], + "path": [ + "jobCandidates", + "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", + "updateInterview", + "3" + ] + } + }, + "response": [] + }, + { + "name": "[Job Candidate is Selected]", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"jobId\": \"{{jobId}}\",\r\n \"userId\": \"b15feb1a-5a9d-456a-86d9-5fd8fad3ac04\",\r\n \"externalId\": \"300234321\",\r\n \"resume\": \"http://example.com\",\r\n \"remark\": \"Job Candidate Notifications Test remark {{testCaseNumber}}\",\r\n \"status\": \"selected\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobCandidates", + "host": [ + "{{URL}}" + ], + "path": [ + "jobCandidates" + ] + } + }, + "response": [] + }, + { + "name": "[Resource Booking is Placed]", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27\",\r\n \"endDate\": \"2020-10-27\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"billingAccountId\": 80000071\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/resourceBookings", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ] + } + }, + "response": [] } ] } diff --git a/scripts/demo-email-notifications/index.js b/scripts/demo-email-notifications/index.js index 23f3ac9f..631a4d94 100644 --- a/scripts/demo-email-notifications/index.js +++ b/scripts/demo-email-notifications/index.js @@ -1,6 +1,8 @@ const Kafka = require('no-kafka') const fs = require('fs') const config = require('config') +const axios = require('axios') +const _ = require('lodash') const moment = require('moment') const handlebars = require('handlebars') const logger = require('../../src/common/logger') @@ -8,6 +10,7 @@ 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 slackURL = null const localLogger = { debug: message => logger.debug({ component: 'render email content', context: 'test', message }), @@ -64,10 +67,15 @@ async function initConsumer () { fs.mkdirSync('out') } if (message.payload.notifications) { - message.payload.notifications.forEach((notification) => { + _.forEach(_.filter(message.payload.notifications, ['serviceId', 'email']), (notification) => { const email = template(notification.details.data) fs.writeFileSync(`./out/${notification.details.data.subject}-${Date.now()}.html`, email) }) + for (const notification of _.filter(message.payload.notifications, ['serviceId', 'slack'])) { + if (slackURL) { + await axios.post(slackURL, { text: notification.details.text, blocks: notification.details.blocks }) + } + } } } } diff --git a/src/common/helper.js b/src/common/helper.js index 1fd28f60..882f6c36 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1914,7 +1914,13 @@ async function getTags (description) { * @returns {Object} the project created */ async function createProject (currentUser, data) { - const token = currentUser.jwtToken + let token + if (currentUser.hasManagePermission || currentUser.isMachine) { + const m2mToken = await getM2MToken() + token = `Bearer ${m2mToken}` + } else { + token = currentUser.jwtToken + } const res = await request .post(`${config.TC_API}/projects/`) .set('Authorization', token) diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index e34fa943..2f3911d9 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -160,8 +160,8 @@ async function suggestMembers (req, res) { * @param req the request * @param res the response */ - async function calculateAmount(req, res) { - res.send(await service.calculateAmount(req.body)); +async function calculateAmount (req, res) { + res.send(await service.calculateAmount(req.body)) } /** @@ -169,8 +169,8 @@ async function suggestMembers (req, res) { * @param req the request * @param res the response */ -async function createPayment(req, res) { - res.send(await service.createPayment(req.body.totalAmount)); +async function createPayment (req, res) { + res.send(await service.createPayment(req.body.totalAmount)) } module.exports = { diff --git a/src/eventHandlers/InterviewEventHandler.js b/src/eventHandlers/InterviewEventHandler.js index 44d055a7..23735827 100644 --- a/src/eventHandlers/InterviewEventHandler.js +++ b/src/eventHandlers/InterviewEventHandler.js @@ -2,8 +2,12 @@ * Handle events for Interview. */ +const { Op } = require('sequelize') +const _ = require('lodash') +const config = require('config') const models = require('../models') -// const logger = require('../common/logger') +const logger = require('../common/logger') +const helper = require('../common/helper') const teamService = require('../services/TeamService') /** @@ -33,6 +37,123 @@ async function sendInvitationEmail (payload) { }) } +/** + * Check if there is overlapping interview, if there is overlapping, then send notifications. + * + * @param {Object} payload the event payload + * @returns {undefined} + */ +async function checkOverlapping (payload) { + const interview = payload.value + const overlappingInterview = await models.Interview.findAll({ + where: { + [Op.or]: [{ + startTimestamp: { + [Op.lt]: interview.endTimestamp, + [Op.gte]: interview.startTimestamp + } + }, { + endTimestamp: { + [Op.lte]: interview.endTimestamp, + [Op.gt]: interview.startTimestamp + } + }, { + [Op.and]: [{ + startTimestamp: { + [Op.lt]: interview.startTimestamp + } + }, { + endTimestamp: { + [Op.gt]: interview.endTimestamp + } + }] + }] + } + }) + if (_.size(overlappingInterview) > 1) { + const template = helper.getEmailTemplatesForKey('notificationEmailTemplates')['taas.notification.interviews-overlapping'] + const jobCandidates = await models.JobCandidate.findAll({ where: { id: _.map(overlappingInterview, 'jobCandidateId') } }) + const jobs = await models.Job.findAll({ where: { id: _.uniq(_.map(jobCandidates, 'jobId')) } }) + + const interviews = [] + for (const oli of overlappingInterview) { + const jobCandidate = _.find(jobCandidates, { id: oli.jobCandidateId }) + const job = _.find(jobs, { id: jobCandidate.jobId }) + const project = await helper.getProjectById({ isMachine: true }, job.projectId) + const user = await helper.getUserById(jobCandidate.userId) + interviews.push({ + teamName: project.name, + teamURL: `${config.TAAS_APP_URL}/${project.id}`, + jobTitle: job.title, + jobURL: `${config.TAAS_APP_URL}/${project.id}/positions/${job.id}`, + candidateUserHandle: user.handle, + startTime: oli.startTimestamp, + endTime: oli.endTimestamp + }) + } + + const emailData = { + serviceId: 'email', + type: 'taas.notification.interviews-overlapping', + details: { + from: template.from, + recipients: template.recipients, + data: { + subject: template.subject, + interviews, + notificationType: { + overlappingInterview: true + }, + description: 'Send notification if there is a new Interview created which overlaps existent interview by time (from "startTimestamp" till "endTimestamp"). Do the same if we update start/end timestamp for Some Interview and now it overlaps with another one' + }, + sendgridTemplateId: template.sendgridTemplateId, + version: 'v3' + } + } + const slackData = { + serviceId: 'slack', + type: 'taas.notification.interviews-overlapping', + details: { + channel: config.NOTIFICATION_SLACK_CHANNEL, + text: template.subject, + blocks: _.flatMap(interviews, iv => [{ + type: 'context', + elements: [{ + type: 'mrkdwn', + text: `teamName: *${iv.teamName}*` + }, { + type: 'mrkdwn', + text: `teamURL: ${iv.teamURL}` + }, { + type: 'mrkdwn', + text: `jobTitle: *${iv.jobTitle}*` + }, { + type: 'mrkdwn', + text: `jobURL: ${iv.jobURL}` + }, { + type: 'mrkdwn', + text: `candidateUserHandle: *${iv.candidateUserHandle}*` + }, { + type: 'mrkdwn', + text: `startTime: *${iv.startTime.toISOString()}*` + }, { + type: 'mrkdwn', + text: `endTime: *${iv.endTime.toISOString()}*` + }] + }, { type: 'divider' }]) + } + } + await helper.postEvent(config.NOTIFICATIONS_CREATE_TOPIC, { + notifications: [emailData, slackData] + }) + logger.debug({ + component: 'InterviewEventHandler', + context: 'checkOverlapping', + message: `interviewIds: ${_.join(_.map(overlappingInterview, 'id'), ',')}` + }) + } +} + /** * Process interview request event. * @@ -41,8 +162,20 @@ async function sendInvitationEmail (payload) { */ async function processRequest (payload) { await sendInvitationEmail(payload) + await checkOverlapping(payload) +} + +/** + * Process interview update event. + * + * @param {Object} payload the event payload + * @returns {undefined} + */ +async function processUpdate (payload) { + await checkOverlapping(payload) } module.exports = { - processRequest + processRequest, + processUpdate } diff --git a/src/eventHandlers/JobCandidateEventHandler.js b/src/eventHandlers/JobCandidateEventHandler.js index 2ad9005b..f93f958b 100644 --- a/src/eventHandlers/JobCandidateEventHandler.js +++ b/src/eventHandlers/JobCandidateEventHandler.js @@ -136,6 +136,85 @@ async function processCreate (payload) { if (payload.value.status === 'placed') { await withDrawnJobCandidates(payload) } + if (payload.value.status === 'selected') { + await sendJobCandidateSelectedNotification(payload) + } +} + +/** + * Send job candidate selected notification. + * + * @param {Object} payload the event payload + * @returns {undefined} + */ +async function sendJobCandidateSelectedNotification (payload) { + const jobCandidate = payload.value + const job = await models.Job.findById(jobCandidate.jobId) + const user = await helper.getUserById(jobCandidate.userId) + const template = helper.getEmailTemplatesForKey('notificationEmailTemplates')['taas.notification.job-candidate-selected'] + const project = await helper.getProjectById({ isMachine: true }, job.projectId) + const jobUrl = `${config.TAAS_APP_URL}/${project.id}/positions/${job.id}` + const emailData = { + serviceId: 'email', + type: 'taas.notification.job-candidate-selected', + details: { + from: template.from, + recipients: template.recipients, + data: { + subject: template.subject, + teamName: project.name, + jobTitle: job.title, + jobDuration: job.duration, + jobStartDate: job.startDate, + userHandle: user.handle, + jobUrl, + notificationType: { + candidateSelected: true + }, + description: 'Send notification if Job Candidate status has been change to "selected" or Job Candidate has been created with "selected" status' + }, + sendgridTemplateId: template.sendgridTemplateId, + version: 'v3' + } + } + const slackData = { + serviceId: 'slack', + type: 'taas.notification.job-candidate-selected', + details: { + channel: config.NOTIFICATION_SLACK_CHANNEL, + text: template.subject, + blocks: [{ + type: 'context', + elements: [{ + type: 'mrkdwn', + text: `teamName: *${project.name}*` + }, { + type: 'mrkdwn', + text: `jobTitle: *${job.title}*` + }, { + type: 'mrkdwn', + text: `jobDuration: *${job.duration}*` + }, { + type: 'mrkdwn', + text: `jobStartDate: *${job.startDate.toISOString()}*` + }, { + type: 'mrkdwn', + text: `userHandle: *${user.handle}*` + }, { + type: 'mrkdwn', + text: `jobUrl: ${jobUrl}` + }] + }] + } + } + await helper.postEvent(config.NOTIFICATIONS_CREATE_TOPIC, { + notifications: [emailData, slackData] + }) + logger.debug({ + component: 'JobCandidateEventHandler', + context: 'sendJobCandidateSelectedNotification', + message: `teamName: ${project.name}, jobTitle: ${payload.value.title}, jobDuration: ${payload.value.duration}, jobStartDate: ${payload.value.startDate}` + }) } /** @@ -149,6 +228,9 @@ async function processUpdate (payload) { if (payload.value.status === 'placed' && payload.options.oldValue.status !== 'placed') { await withDrawnJobCandidates(payload) } + if (payload.value.status === 'selected' && payload.options.oldValue.status !== 'selected') { + await sendJobCandidateSelectedNotification(payload) + } } module.exports = { diff --git a/src/eventHandlers/JobEventHandler.js b/src/eventHandlers/JobEventHandler.js index e1938de2..f461e7ad 100644 --- a/src/eventHandlers/JobEventHandler.js +++ b/src/eventHandlers/JobEventHandler.js @@ -3,6 +3,8 @@ */ const { Op } = require('sequelize') +const config = require('config') +const _ = require('lodash') const models = require('../models') const logger = require('../common/logger') const helper = require('../common/helper') @@ -65,6 +67,55 @@ async function processUpdate (payload) { await cancelJob(payload) } +/** + * Process job create event. + * + * @param {Object} payload the event payload + * @returns {undefined} + */ +async function processCreate (payload) { + if (payload.options.onTeamCreating) { + logger.debug({ + component: 'JobEventHandler', + context: 'jobCreate', + message: 'skip these jobs which are created together with the Team' + }) + return + } + const template = helper.getEmailTemplatesForKey('notificationEmailTemplates')['taas.notification.job-created'] + const project = await helper.getProjectById({ isMachine: true }, payload.value.projectId) + const emailData = { + serviceId: 'email', + type: 'taas.notification.job-created', + details: { + from: template.from, + recipients: _.map(project.members, m => _.pick(m, 'email')), + data: { + subject: template.subject, + teamName: project.name, + jobTitle: payload.value.title, + jobDuration: payload.value.duration, + jobStartDate: payload.value.startDate, + notificationType: { + newJobCreated: true + }, + description: 'Send notification a new Job was created' + }, + sendgridTemplateId: template.sendgridTemplateId, + version: 'v3' + } + } + await helper.postEvent(config.NOTIFICATIONS_CREATE_TOPIC, { + notifications: [emailData] + }) + logger.debug({ + component: 'JobEventHandler', + context: 'jobCreate', + message: `teamName: ${project.name}, jobTitle: ${payload.value.title}, jobDuration: ${payload.value.duration}, jobStartDate: ${payload.value.startDate}` + }) +} + module.exports = { - processUpdate + processUpdate, + processCreate } diff --git a/src/eventHandlers/ResourceBookingEventHandler.js b/src/eventHandlers/ResourceBookingEventHandler.js index 067deb75..ff79c7fc 100644 --- a/src/eventHandlers/ResourceBookingEventHandler.js +++ b/src/eventHandlers/ResourceBookingEventHandler.js @@ -4,6 +4,7 @@ const { Op } = require('sequelize') const _ = require('lodash') +const config = require('config') const models = require('../models') const logger = require('../common/logger') const helper = require('../common/helper') @@ -67,6 +68,50 @@ async function placeJobCandidate (payload) { message: `id: ${result.id} candidate got selected.` }) }))) + const template = helper.getEmailTemplatesForKey('notificationEmailTemplates')['taas.notification.resource-booking-placed'] + const project = await helper.getProjectById({ isMachine: true }, resourceBooking.projectId) + const user = await helper.getUserById(resourceBooking.userId) + const job = await models.Job.findById(resourceBooking.jobId) + const recipients = _.map(project.members, m => _.pick(m, 'email')) + const emailData = { + serviceId: 'email', + type: 'taas.notification.resource-booking-placed', + details: { + from: template.from, + recipients, + data: { + subject: template.subject, + teamName: project.name, + jobTitle: job.title, + userHandle: user.handle, + startDate: resourceBooking.startDate, + endDate: resourceBooking.endDate, + notificationType: { + resourceBookingPlaced: true + }, + description: 'Send notification if Resource Bookings was created with status "placed" or existent record updated to status "placed"' + }, + sendgridTemplateId: template.sendgridTemplateId, + version: 'v3' + } + } + const webData = { + serviceId: 'web', + type: 'taas.notification.resource-booking-placed', + details: { + recipients, + contents: { teamName: project.name, projectId: project.id, userHandle: user.handle, jobTitle: job.title }, + version: 1 + } + } + await helper.postEvent(config.NOTIFICATIONS_CREATE_TOPIC, { + notifications: [emailData, webData] + }) + logger.debug({ + component: 'ResourceBookingEventHandler', + context: 'placeJobCandidate', + message: `send notifications, teamName: ${project.name}, jobTitle: ${job.title}, projectId: ${project.id}, userHandle: ${user.handle}` + }) } /** diff --git a/src/eventHandlers/TeamEventHandler.js b/src/eventHandlers/TeamEventHandler.js new file mode 100644 index 00000000..3a1fceb3 --- /dev/null +++ b/src/eventHandlers/TeamEventHandler.js @@ -0,0 +1,59 @@ +/* + * Handle events for Team. + */ + +const _ = require('lodash') +const config = require('config') +const logger = require('../common/logger') +const helper = require('../common/helper') + +/** + * Once we create a team, the notification emails to be sent out. + * + * @param {Object} payload the event payload + * @returns {undefined} + */ +async function sendNotificationEmail (payload) { + const template = helper.getEmailTemplatesForKey('notificationEmailTemplates')['taas.notification.team-created'] + const emailData = { + serviceId: 'email', + type: 'taas.notification.team-created', + details: { + from: template.from, + recipients: _.map(payload.project.members, m => _.pick(m, 'email')), + data: { + subject: template.subject, + teamName: payload.project.name, + jobList: _.map(payload.jobs, j => _.pick(j, 'title', 'duration', 'startDate')), + notificationType: { + newTeamCreated: true + }, + description: 'Send notification when a new Team was created using endpoint "POST /taas-teams/submitTeamRequest"' + }, + sendgridTemplateId: template.sendgridTemplateId, + version: 'v3' + } + } + await helper.postEvent(config.NOTIFICATIONS_CREATE_TOPIC, { + notifications: [emailData] + }) + logger.debug({ + component: 'TeamEventHandler', + context: 'sendNotificationEmail', + message: `project id: ${payload.project.id} created with jobs: ${_.join(_.map(payload.jobs, 'id'), ',')}` + }) +} + +/** + * Process team creating event. + * + * @param {Object} payload the event payload + * @returns {undefined} + */ +async function processCreate (payload) { + await sendNotificationEmail(payload) +} + +module.exports = { + processCreate +} diff --git a/src/eventHandlers/index.js b/src/eventHandlers/index.js index 3f308b18..1992fc61 100644 --- a/src/eventHandlers/index.js +++ b/src/eventHandlers/index.js @@ -10,9 +10,11 @@ const ResourceBookingEventHandler = require('./ResourceBookingEventHandler') const InterviewEventHandler = require('./InterviewEventHandler') const RoleEventHandler = require('./RoleEventHandler') const WorkPeriodPaymentEventHandler = require('./WorkPeriodPaymentEventHandler') +const TeamEventHandler = require('./TeamEventHandler') const logger = require('../common/logger') const TopicOperationMapping = { + [config.TAAS_JOB_CREATE_TOPIC]: JobEventHandler.processCreate, [config.TAAS_JOB_UPDATE_TOPIC]: JobEventHandler.processUpdate, [config.TAAS_JOB_CANDIDATE_CREATE_TOPIC]: JobCandidateEventHandler.processCreate, [config.TAAS_JOB_CANDIDATE_UPDATE_TOPIC]: JobCandidateEventHandler.processUpdate, @@ -22,7 +24,9 @@ const TopicOperationMapping = { [config.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC]: WorkPeriodPaymentEventHandler.processCreate, [config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC]: WorkPeriodPaymentEventHandler.processUpdate, [config.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewEventHandler.processRequest, - [config.TAAS_ROLE_DELETE_TOPIC]: RoleEventHandler.processDelete + [config.TAAS_INTERVIEW_UPDATE_TOPIC]: InterviewEventHandler.processUpdate, + [config.TAAS_ROLE_DELETE_TOPIC]: RoleEventHandler.processDelete, + [config.TAAS_TEAM_CREATE_TOPIC]: TeamEventHandler.processCreate } /** diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index afc4bf87..2dfd79c6 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -108,20 +108,20 @@ module.exports = { scopes: [] } }, - "/taas-teams/calculateAmount": { + '/taas-teams/calculateAmount': { post: { - controller: "TeamController", - method: "calculateAmount", + controller: 'TeamController', + method: 'calculateAmount', auth: 'jwt', scopes: [constants.Scopes.CREATE_TAAS_TEAM] - }, + } }, - "/taas-teams/createPayment": { + '/taas-teams/createPayment': { post: { - controller: "TeamController", - method: "createPayment", + controller: 'TeamController', + method: 'createPayment', auth: 'jwt', scopes: [constants.Scopes.CREATE_TAAS_TEAM] - }, -} + } + } } diff --git a/src/services/JobService.js b/src/services/JobService.js index 4a183783..7e38227c 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -163,7 +163,7 @@ getJob.schema = Joi.object().keys({ * @params {Object} job the job to be created * @returns {Object} the created job */ -async function createJob (currentUser, job) { +async function createJob (currentUser, job, onTeamCreating) { // check user permission if (!currentUser.hasManagePermission && !currentUser.isMachine) { await helper.checkIsMemberOfProject(currentUser.userId, job.projectId) @@ -183,7 +183,7 @@ async function createJob (currentUser, job) { job.createdBy = await helper.getUserId(currentUser.userId) const created = await Job.create(job) - await helper.postEvent(config.TAAS_JOB_CREATE_TOPIC, created.toJSON()) + await helper.postEvent(config.TAAS_JOB_CREATE_TOPIC, created.toJSON(), { onTeamCreating }) return created.toJSON() } @@ -213,7 +213,8 @@ createJob.schema = Joi.object() currency: Joi.stringAllowEmpty().allow(null), roleIds: Joi.array().items(Joi.string().uuid().required()) }) - .required() + .required(), + onTeamCreating: Joi.boolean().default(false) }) .required() diff --git a/src/services/TeamService.js b/src/services/TeamService.js index f6cd6612..ef6a45dc 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -9,6 +9,7 @@ const config = require('config') const helper = require('../common/helper') const logger = require('../common/logger') const errors = require('../common/errors') +const eventDispatcher = require('../common/eventDispatcher') const JobService = require('./JobService') const ResourceBookingService = require('./ResourceBookingService') const HttpStatus = require('http-status-codes') @@ -19,7 +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")(config.STRIPE_SECRET_KEY,{maxNetworkRetries: 5}); +const stripe = require('stripe')(config.STRIPE_SECRET_KEY, { maxNetworkRetries: 5 }) const emailTemplates = helper.getEmailTemplatesForKey('teamTemplates') @@ -1041,7 +1042,7 @@ async function createTeam (currentUser, data) { // create project with given data const project = await helper.createProject(currentUser, projectRequestBody) // create jobs for the given positions. - await Promise.all(_.map(data.positions, async position => { + const jobs = await Promise.all(_.map(data.positions, async position => { const roleSearchRequest = roleSearchRequests[position.roleSearchRequestId] const job = { projectId: project.id, @@ -1061,8 +1062,9 @@ async function createTeam (currentUser, data) { if (position.durationWeeks) { job.duration = position.durationWeeks } - await JobService.createJob(currentUser, job) + return await JobService.createJob(currentUser, job, true) })) + await eventDispatcher.handleEvent(config.TAAS_TEAM_CREATE_TOPIC, { project, jobs }) return { projectId: project.id } } @@ -1163,10 +1165,9 @@ suggestMembers.schema = Joi.object().keys({ * @param {Object} amount * @returns {int} totalAmount */ - async function calculateAmount(amount) { - let totalAmount = 0; - _.forEach(amount, amt => totalAmount += amt.numberOfResources * amt.rate) - return { totalAmount }; +async function calculateAmount (amount) { + const totalAmount = _.sum(_.map(amount, amt => amt.numberOfResources * amt.rate)) + return { totalAmount } } /** @@ -1174,16 +1175,15 @@ suggestMembers.schema = Joi.object().keys({ * @param {int} totalAmount * @returns {string} paymentIntentToken */ -async function createPayment(totalAmount) { - const dollarToCents = (totalAmount*100); +async function createPayment (totalAmount) { + const dollarToCents = (totalAmount * 100) const paymentIntent = await stripe.paymentIntents.create({ amount: dollarToCents, - currency: config.CURRENCY, - }); - return { paymentIntentToken: paymentIntent.client_secret }; + currency: config.CURRENCY + }) + return { paymentIntentToken: paymentIntent.client_secret } } - module.exports = { searchTeams, getTeam, From b6bff7e9e4e860a572ba0d84204f6aa42829b447 Mon Sep 17 00:00:00 2001 From: xxcxy Date: Fri, 13 Aug 2021 22:07:10 +0800 Subject: [PATCH 2/2] refactor --- config/default.js | 4 +- data/notifications-email-template.html | 26 ++++----- scripts/demo-email-notifications/README.md | 11 ++-- scripts/demo-email-notifications/index.js | 5 +- src/common/helper.js | 24 ++++++--- src/eventHandlers/InterviewEventHandler.js | 54 ++++++++++--------- src/eventHandlers/JobCandidateEventHandler.js | 15 +++--- src/eventHandlers/JobEventHandler.js | 20 +++++-- .../ResourceBookingEventHandler.js | 22 +++++++- src/eventHandlers/TeamEventHandler.js | 10 +++- 10 files changed, 119 insertions(+), 72 deletions(-) diff --git a/config/default.js b/config/default.js index 4f31db75..bf498d2e 100644 --- a/config/default.js +++ b/config/default.js @@ -274,5 +274,7 @@ module.exports = { RESOURCE_BOOKING_EXPIRY_TIME: process.env.RESOURCE_BOOKING_EXPIRY_TIME || 'P21D', // The Stripe STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, - CURRENCY: process.env.CURRENCY || 'usd' + CURRENCY: process.env.CURRENCY || 'usd', + // The slack webhook url to send slack notifications + SLACK_WEBHOOK_URL: process.env.SLACK_WEBHOOK_URL } diff --git a/data/notifications-email-template.html b/data/notifications-email-template.html index 6d263281..fd213800 100644 --- a/data/notifications-email-template.html +++ b/data/notifications-email-template.html @@ -242,7 +242,7 @@ {{/if}} {{#if notificationType.newTeamCreated}} Team Name: - {{teamName}} + {{teamName}} @@ -251,7 +251,7 @@ {{#each jobList}} - + @@ -267,8 +267,8 @@ - - + + @@ -278,19 +278,15 @@
Job Title
{{this.title}}{{this.title}} {{this.duration}} {{this.startDate}}
Start Date
{{teamName}}{{jobTitle}}{{teamName}}{{jobTitle}} {{jobDuration}} {{jobStartDate}}
- - {{#each interviews}} - - - - + + @@ -303,15 +299,13 @@ - - - - + + @@ -328,8 +322,8 @@ - - + + diff --git a/scripts/demo-email-notifications/README.md b/scripts/demo-email-notifications/README.md index 3a5dbd47..e55e275d 100644 --- a/scripts/demo-email-notifications/README.md +++ b/scripts/demo-email-notifications/README.md @@ -18,19 +18,24 @@ This script does 2 things: INTERVIEW_COMING_UP_MATCH_WINDOW=PT1M INTERVIEW_COMPLETED_MATCH_WINDOW=PT1M ``` +2. Config `SLACK_WEBHOOK_URL` env, if you want to send slack notifications -2. Recreate demo data by: + ```sh + SLACK_WEBHOOK_URL=https://hooks.slack.com/services/*** + ``` + +3. Recreate demo data by: ```sh npm run local:init` -3. Run TaaS API by: +4. Run TaaS API by: ```sh npm run dev ``` -4. Run this demo script: +5. Run this demo script: ```sh node scripts/demo-email-notifications diff --git a/scripts/demo-email-notifications/index.js b/scripts/demo-email-notifications/index.js index 631a4d94..761541ff 100644 --- a/scripts/demo-email-notifications/index.js +++ b/scripts/demo-email-notifications/index.js @@ -10,7 +10,6 @@ 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 slackURL = null const localLogger = { debug: message => logger.debug({ component: 'render email content', context: 'test', message }), @@ -72,8 +71,8 @@ async function initConsumer () { fs.writeFileSync(`./out/${notification.details.data.subject}-${Date.now()}.html`, email) }) for (const notification of _.filter(message.payload.notifications, ['serviceId', 'slack'])) { - if (slackURL) { - await axios.post(slackURL, { text: notification.details.text, blocks: notification.details.blocks }) + if (config.SLACK_WEBHOOK_URL) { + await axios.post(config.SLACK_WEBHOOK_URL, { text: notification.details.text, blocks: notification.details.blocks }) } } } diff --git a/src/common/helper.js b/src/common/helper.js index 882f6c36..5f96e172 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1914,13 +1914,7 @@ async function getTags (description) { * @returns {Object} the project created */ async function createProject (currentUser, data) { - let token - if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken() - token = `Bearer ${m2mToken}` - } else { - token = currentUser.jwtToken - } + const token = currentUser.jwtToken const res = await request .post(`${config.TC_API}/projects/`) .set('Authorization', token) @@ -2047,6 +2041,19 @@ function getEmailTemplatesForKey (key) { }) } +/** + * Format date to be used in email + * + * @param {Date} date date to be formatted + * @returns {String} formatted date + */ +function formatDate (date) { + if (date) { + const tzName = date.toLocaleString('en', { timeZoneName: 'short' }).split(' ').pop() + return `${moment(date).format('MMM D, YYYY, h:mm:ss a')} ${tzName}` + } +} + module.exports = { encodeQueryString, getParamFromCliArgs, @@ -2110,5 +2117,6 @@ module.exports = { getMemberGroups, removeTextFormatting, getMembersSuggest, - getEmailTemplatesForKey + getEmailTemplatesForKey, + formatDate } diff --git a/src/eventHandlers/InterviewEventHandler.js b/src/eventHandlers/InterviewEventHandler.js index 23735827..48bad2f2 100644 --- a/src/eventHandlers/InterviewEventHandler.js +++ b/src/eventHandlers/InterviewEventHandler.js @@ -9,6 +9,7 @@ const models = require('../models') const logger = require('../common/logger') const helper = require('../common/helper') const teamService = require('../services/TeamService') +const Constants = require('../../app-constants') /** * Once we request Interview for a JobCandidate, the invitation emails to be sent out. @@ -45,27 +46,34 @@ async function sendInvitationEmail (payload) { */ async function checkOverlapping (payload) { const interview = payload.value + if (_.includes([Constants.Interviews.Status.Cancelled, Constants.Interviews.Status.Completed], interview.status)) { + return + } const overlappingInterview = await models.Interview.findAll({ where: { - [Op.or]: [{ - startTimestamp: { - [Op.lt]: interview.endTimestamp, - [Op.gte]: interview.startTimestamp - } - }, { - endTimestamp: { - [Op.lte]: interview.endTimestamp, - [Op.gt]: interview.startTimestamp - } + [Op.and]: [{ + status: _.values(_.omit(Constants.Interviews.Status, 'Completed', 'Cancelled')) }, { - [Op.and]: [{ + [Op.or]: [{ startTimestamp: { - [Op.lt]: interview.startTimestamp + [Op.lt]: interview.endTimestamp, + [Op.gte]: interview.startTimestamp } }, { endTimestamp: { - [Op.gt]: interview.endTimestamp + [Op.lte]: interview.endTimestamp, + [Op.gt]: interview.startTimestamp } + }, { + [Op.and]: [{ + startTimestamp: { + [Op.lt]: interview.startTimestamp + } + }, { + endTimestamp: { + [Op.gt]: interview.endTimestamp + } + }] }] }] } @@ -87,8 +95,8 @@ async function checkOverlapping (payload) { jobTitle: job.title, jobURL: `${config.TAAS_APP_URL}/${project.id}/positions/${job.id}`, candidateUserHandle: user.handle, - startTime: oli.startTimestamp, - endTime: oli.endTimestamp + startTime: helper.formatDate(oli.startTimestamp), + endTime: helper.formatDate(oli.endTimestamp) }) } @@ -104,7 +112,7 @@ async function checkOverlapping (payload) { notificationType: { overlappingInterview: true }, - description: 'Send notification if there is a new Interview created which overlaps existent interview by time (from "startTimestamp" till "endTimestamp"). Do the same if we update start/end timestamp for Some Interview and now it overlaps with another one' + description: 'Overlapping Interview Invites' }, sendgridTemplateId: template.sendgridTemplateId, version: 'v3' @@ -120,25 +128,19 @@ async function checkOverlapping (payload) { type: 'context', elements: [{ type: 'mrkdwn', - text: `teamName: *${iv.teamName}*` - }, { - type: 'mrkdwn', - text: `teamURL: ${iv.teamURL}` - }, { - type: 'mrkdwn', - text: `jobTitle: *${iv.jobTitle}*` + text: `teamName: <${iv.teamURL}|*${iv.teamName}*>` }, { type: 'mrkdwn', - text: `jobURL: ${iv.jobURL}` + text: `jobTitle: <${iv.jobURL}|*${iv.jobTitle}*>` }, { type: 'mrkdwn', text: `candidateUserHandle: *${iv.candidateUserHandle}*` }, { type: 'mrkdwn', - text: `startTime: *${iv.startTime.toISOString()}*` + text: `startTime: *${helper.formatDate(iv.startTime)}*` }, { type: 'mrkdwn', - text: `endTime: *${iv.endTime.toISOString()}*` + text: `endTime: *${helper.formatDate(iv.endTime)}*` }] }, { type: 'divider' }]) } diff --git a/src/eventHandlers/JobCandidateEventHandler.js b/src/eventHandlers/JobCandidateEventHandler.js index f93f958b..424f8fd1 100644 --- a/src/eventHandlers/JobCandidateEventHandler.js +++ b/src/eventHandlers/JobCandidateEventHandler.js @@ -154,6 +154,7 @@ async function sendJobCandidateSelectedNotification (payload) { const template = helper.getEmailTemplatesForKey('notificationEmailTemplates')['taas.notification.job-candidate-selected'] const project = await helper.getProjectById({ isMachine: true }, job.projectId) const jobUrl = `${config.TAAS_APP_URL}/${project.id}/positions/${job.id}` + const teamUrl = `${config.TAAS_APP_URL}/${project.id}` const emailData = { serviceId: 'email', type: 'taas.notification.job-candidate-selected', @@ -163,15 +164,16 @@ async function sendJobCandidateSelectedNotification (payload) { data: { subject: template.subject, teamName: project.name, + teamUrl, jobTitle: job.title, jobDuration: job.duration, - jobStartDate: job.startDate, + jobStartDate: helper.formatDate(job.startDate), userHandle: user.handle, jobUrl, notificationType: { candidateSelected: true }, - description: 'Send notification if Job Candidate status has been change to "selected" or Job Candidate has been created with "selected" status' + description: 'Job Candidate is Selected' }, sendgridTemplateId: template.sendgridTemplateId, version: 'v3' @@ -187,22 +189,19 @@ async function sendJobCandidateSelectedNotification (payload) { type: 'context', elements: [{ type: 'mrkdwn', - text: `teamName: *${project.name}*` + text: `teamName: <${teamUrl}|*${project.name}*>` }, { type: 'mrkdwn', - text: `jobTitle: *${job.title}*` + text: `jobTitle: <${jobUrl}|*${job.title}*>` }, { type: 'mrkdwn', text: `jobDuration: *${job.duration}*` }, { type: 'mrkdwn', - text: `jobStartDate: *${job.startDate.toISOString()}*` + text: `jobStartDate: *${helper.formatDate(job.startDate)}*` }, { type: 'mrkdwn', text: `userHandle: *${user.handle}*` - }, { - type: 'mrkdwn', - text: `jobUrl: ${jobUrl}` }] }] } diff --git a/src/eventHandlers/JobEventHandler.js b/src/eventHandlers/JobEventHandler.js index f461e7ad..50a2f537 100644 --- a/src/eventHandlers/JobEventHandler.js +++ b/src/eventHandlers/JobEventHandler.js @@ -68,12 +68,12 @@ async function processUpdate (payload) { } /** - * Process job create event. + * When Job is created, send notification to user. * * @param {Object} payload the event payload * @returns {undefined} */ -async function processCreate (payload) { +async function sendNotifications (payload) { if (payload.options.onTeamCreating) { logger.debug({ component: 'JobEventHandler', @@ -93,13 +93,15 @@ async function processCreate (payload) { data: { subject: template.subject, teamName: project.name, + teamURL: `${config.TAAS_APP_URL}/${project.id}`, jobTitle: payload.value.title, + jobURL: `${config.TAAS_APP_URL}/${project.id}/positions/${payload.value.id}`, jobDuration: payload.value.duration, - jobStartDate: payload.value.startDate, + jobStartDate: helper.formatDate(payload.value.startDate), notificationType: { newJobCreated: true }, - description: 'Send notification a new Job was created' + description: 'New Job created' }, sendgridTemplateId: template.sendgridTemplateId, version: 'v3' @@ -115,6 +117,16 @@ async function processCreate (payload) { }) } +/** + * Process job create event. + * + * @param {Object} payload the event payload + * @returns {undefined} + */ +async function processCreate (payload) { + await sendNotifications(payload) +} + module.exports = { processUpdate, processCreate diff --git a/src/eventHandlers/ResourceBookingEventHandler.js b/src/eventHandlers/ResourceBookingEventHandler.js index ff79c7fc..5a1f15a0 100644 --- a/src/eventHandlers/ResourceBookingEventHandler.js +++ b/src/eventHandlers/ResourceBookingEventHandler.js @@ -68,11 +68,27 @@ async function placeJobCandidate (payload) { message: `id: ${result.id} candidate got selected.` }) }))) +} + +/** + * When ResourceBooking's status is changed to `placed` + * send notifications to user + * + * @param {Object} payload the event payload + * @returns {undefined} + */ +async function sendPlacedNotifications (payload) { + if (payload.value.status !== 'placed' || _.get(payload, 'options.oldValue.status') === 'placed') { + return + } + const resourceBooking = await models.ResourceBooking.findById(payload.value.id) const template = helper.getEmailTemplatesForKey('notificationEmailTemplates')['taas.notification.resource-booking-placed'] const project = await helper.getProjectById({ isMachine: true }, resourceBooking.projectId) const user = await helper.getUserById(resourceBooking.userId) const job = await models.Job.findById(resourceBooking.jobId) const recipients = _.map(project.members, m => _.pick(m, 'email')) + const jobUrl = `${config.TAAS_APP_URL}/${project.id}/positions/${job.id}` + const teamUrl = `${config.TAAS_APP_URL}/${project.id}` const emailData = { serviceId: 'email', type: 'taas.notification.resource-booking-placed', @@ -82,14 +98,16 @@ async function placeJobCandidate (payload) { data: { subject: template.subject, teamName: project.name, + teamUrl, jobTitle: job.title, + jobUrl, userHandle: user.handle, startDate: resourceBooking.startDate, endDate: resourceBooking.endDate, notificationType: { resourceBookingPlaced: true }, - description: 'Send notification if Resource Bookings was created with status "placed" or existent record updated to status "placed"' + description: 'Resource Booking is Placed' }, sendgridTemplateId: template.sendgridTemplateId, version: 'v3' @@ -386,6 +404,7 @@ async function processCreate (payload) { await placeJobCandidate(payload) await assignJob(payload) await createWorkPeriods(payload) + await sendPlacedNotifications(payload) } /** @@ -398,6 +417,7 @@ async function processUpdate (payload) { await placeJobCandidate(payload) await assignJob(payload) await updateWorkPeriods(payload) + await sendPlacedNotifications(payload) } /** diff --git a/src/eventHandlers/TeamEventHandler.js b/src/eventHandlers/TeamEventHandler.js index 3a1fceb3..51ad266b 100644 --- a/src/eventHandlers/TeamEventHandler.js +++ b/src/eventHandlers/TeamEventHandler.js @@ -24,11 +24,17 @@ async function sendNotificationEmail (payload) { data: { subject: template.subject, teamName: payload.project.name, - jobList: _.map(payload.jobs, j => _.pick(j, 'title', 'duration', 'startDate')), + teamUrl: `${config.TAAS_APP_URL}/${payload.project.id}`, + jobList: _.map(payload.jobs, j => ({ + title: j.title, + duration: j.duration, + startDate: helper.formatDate(j.startDate), + jobUrl: `${config.TAAS_APP_URL}/${payload.project.id}/positions/${j.id}` + })), notificationType: { newTeamCreated: true }, - description: 'Send notification when a new Team was created using endpoint "POST /taas-teams/submitTeamRequest"' + description: 'New Team created' }, sendgridTemplateId: template.sendgridTemplateId, version: 'v3'
Team NameTeam URL Job titleJob URL Job Candidate Interview Start Date Interview End Date
{{this.teamName}}{{this.teamURL}}{{this.jobTitle}}{{this.jobURL}}{{this.teamName}}{{this.jobTitle}} {{this.candidateUserHandle}} {{this.startTime}} {{this.endTime}}
Team Name Job titleJob URL Job Start Date Job Duration Candidate User Handle
{{teamName}}{{jobTitle}}{{jobUrl}}{{teamName}}{{jobTitle}} {{jobStartDate}} {{jobDuration}} {{userHandle}}Resource Bookings End Date
{{teamName}}{{jobTitle}}{{teamName}}{{jobTitle}} {{userHandle}} {{startDate}} {{endDate}}