diff --git a/config/default.js b/config/default.js
index 1a012a6d..bf498d2e 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
@@ -268,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/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..fd213800 100644
--- a/data/notifications-email-template.html
+++ b/data/notifications-email-template.html
@@ -240,6 +240,96 @@
{{/each}}
{{/if}}
+ {{#if notificationType.newTeamCreated}}
+ Team Name:
+ {{teamName}}
+
+
+ | Job Title |
+ Duration |
+ Start Date |
+
+ {{#each jobList}}
+
+ | {{this.title}} |
+ {{this.duration}} |
+ {{this.startDate}} |
+
+ {{/each}}
+
+ {{/if}}
+ {{#if notificationType.newJobCreated}}
+
+
+ | Team Name |
+ Job title |
+ Duration |
+ Start Date |
+
+
+ | {{teamName}} |
+ {{jobTitle}} |
+ {{jobDuration}} |
+ {{jobStartDate}} |
+
+
+ {{/if}}
+ {{#if notificationType.overlappingInterview}}
+
+
+ | Team Name |
+ Job title |
+ Job Candidate |
+ Interview Start Date |
+ Interview End Date |
+
+ {{#each interviews}}
+
+ | {{this.teamName}} |
+ {{this.jobTitle}} |
+ {{this.candidateUserHandle}} |
+ {{this.startTime}} |
+ {{this.endTime}} |
+
+ {{/each}}
+
+ {{/if}}
+ {{#if notificationType.candidateSelected}}
+
+
+ | Team Name |
+ Job title |
+ Job Start Date |
+ Job Duration |
+ Candidate User Handle |
+
+
+ | {{teamName}} |
+ {{jobTitle}} |
+ {{jobStartDate}} |
+ {{jobDuration}} |
+ {{userHandle}} |
+
+
+ {{/if}}
+ {{#if notificationType.resourceBookingPlaced}}
+
+
+ | Team Name |
+ Job Title |
+ Resource Bookings Handle |
+ Resource Bookings Start Date |
+ Resource 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/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 23f3ac9f..761541ff 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')
@@ -64,10 +66,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 (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 1fd28f60..5f96e172 100644
--- a/src/common/helper.js
+++ b/src/common/helper.js
@@ -2041,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,
@@ -2104,5 +2117,6 @@ module.exports = {
getMemberGroups,
removeTextFormatting,
getMembersSuggest,
- getEmailTemplatesForKey
+ getEmailTemplatesForKey,
+ formatDate
}
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..48bad2f2 100644
--- a/src/eventHandlers/InterviewEventHandler.js
+++ b/src/eventHandlers/InterviewEventHandler.js
@@ -2,9 +2,14 @@
* 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')
+const Constants = require('../../app-constants')
/**
* Once we request Interview for a JobCandidate, the invitation emails to be sent out.
@@ -33,6 +38,124 @@ 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
+ if (_.includes([Constants.Interviews.Status.Cancelled, Constants.Interviews.Status.Completed], interview.status)) {
+ return
+ }
+ const overlappingInterview = await models.Interview.findAll({
+ where: {
+ [Op.and]: [{
+ status: _.values(_.omit(Constants.Interviews.Status, 'Completed', 'Cancelled'))
+ }, {
+ [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: helper.formatDate(oli.startTimestamp),
+ endTime: helper.formatDate(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: 'Overlapping Interview Invites'
+ },
+ 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.teamURL}|*${iv.teamName}*>`
+ }, {
+ type: 'mrkdwn',
+ text: `jobTitle: <${iv.jobURL}|*${iv.jobTitle}*>`
+ }, {
+ type: 'mrkdwn',
+ text: `candidateUserHandle: *${iv.candidateUserHandle}*`
+ }, {
+ type: 'mrkdwn',
+ text: `startTime: *${helper.formatDate(iv.startTime)}*`
+ }, {
+ type: 'mrkdwn',
+ text: `endTime: *${helper.formatDate(iv.endTime)}*`
+ }]
+ }, { 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 +164,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..424f8fd1 100644
--- a/src/eventHandlers/JobCandidateEventHandler.js
+++ b/src/eventHandlers/JobCandidateEventHandler.js
@@ -136,6 +136,84 @@ 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 teamUrl = `${config.TAAS_APP_URL}/${project.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,
+ teamUrl,
+ jobTitle: job.title,
+ jobDuration: job.duration,
+ jobStartDate: helper.formatDate(job.startDate),
+ userHandle: user.handle,
+ jobUrl,
+ notificationType: {
+ candidateSelected: true
+ },
+ description: 'Job Candidate is Selected'
+ },
+ 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: <${teamUrl}|*${project.name}*>`
+ }, {
+ type: 'mrkdwn',
+ text: `jobTitle: <${jobUrl}|*${job.title}*>`
+ }, {
+ type: 'mrkdwn',
+ text: `jobDuration: *${job.duration}*`
+ }, {
+ type: 'mrkdwn',
+ text: `jobStartDate: *${helper.formatDate(job.startDate)}*`
+ }, {
+ type: 'mrkdwn',
+ text: `userHandle: *${user.handle}*`
+ }]
+ }]
+ }
+ }
+ 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 +227,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..50a2f537 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,67 @@ async function processUpdate (payload) {
await cancelJob(payload)
}
+/**
+ * When Job is created, send notification to user.
+ *
+ * @param {Object} payload the event payload
+ * @returns {undefined}
+ */
+async function sendNotifications (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,
+ 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: helper.formatDate(payload.value.startDate),
+ notificationType: {
+ newJobCreated: true
+ },
+ description: 'New Job 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}`
+ })
+}
+
+/**
+ * Process job create event.
+ *
+ * @param {Object} payload the event payload
+ * @returns {undefined}
+ */
+async function processCreate (payload) {
+ await sendNotifications(payload)
+}
+
module.exports = {
- processUpdate
+ processUpdate,
+ processCreate
}
diff --git a/src/eventHandlers/ResourceBookingEventHandler.js b/src/eventHandlers/ResourceBookingEventHandler.js
index 067deb75..5a1f15a0 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')
@@ -69,6 +70,68 @@ async function placeJobCandidate (payload) {
})))
}
+/**
+ * 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',
+ details: {
+ from: template.from,
+ recipients,
+ 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: 'Resource Booking is 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}`
+ })
+}
+
/**
* Update the status of the Job to assigned when it positions requirement is fulfilled.
*
@@ -341,6 +404,7 @@ async function processCreate (payload) {
await placeJobCandidate(payload)
await assignJob(payload)
await createWorkPeriods(payload)
+ await sendPlacedNotifications(payload)
}
/**
@@ -353,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
new file mode 100644
index 00000000..51ad266b
--- /dev/null
+++ b/src/eventHandlers/TeamEventHandler.js
@@ -0,0 +1,65 @@
+/*
+ * 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,
+ 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: 'New Team created'
+ },
+ 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,