Skip to content

Commit 3b06e71

Browse files
author
Md Mahidul Haque Alvi
authored
Merge branch 'feature/interview-nylas' into feature/interview-nylas
2 parents 8586889 + 1246022 commit 3b06e71

22 files changed

+1728
-99
lines changed

README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@
4040
AUTH0_AUDIENCE_UBAHN=
4141
AUTH0_CLIENT_ID=
4242
AUTH0_CLIENT_SECRET=
43-
# necessary if you'll utilize email functionality of interviews
44-
INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID=
45-
INTERVIEW_INVITATION_SENDER_EMAIL=
43+
# Get nylas client id and secret from nylas developer page
44+
NYLAS_CLIENT_ID=
45+
NYLAS_CLIENT_SECRET=
4646
# Locally deployed services (via docker-compose)
4747
ES_HOST=http://dockerhost:9200
4848
DATABASE_URL=postgres://postgres:postgres@dockerhost:5432/postgres
@@ -263,6 +263,34 @@ npm run data:import -- --file path/to-file.json
263263
264264
- List of models that will be imported are defined in `scripts/data/importData.js`.
265265
266+
## Nylas Webhook verification
267+
268+
### schedule
269+
ngrok http 3000
270+
271+
in https://dashboard.nylas.com/applications/{id} create nylas webhook url with created/updated/deleted event trigger
272+
```
273+
https://{generatedId}.ngrok.io/api/v5/taas/nylas-webhooks
274+
```
275+
276+
277+
create job, job candidate, and request interview
278+
279+
You will get a invitation demo email in out/Please select your available time-xxxx.html, open it in broswer
280+
281+
go to `https://schedule.nylas.com/{nylasSlug}` to schedule a time.
282+
283+
In Postman, invoke `Get interview by id` to see the interview status
284+
285+
### cancel
286+
In your email, click `cancel` to cancel interview
287+
288+
Verify inteview status by postman
289+
290+
### reschedule
291+
The webhook didn't support it(schedule and reschedule have same event), ignore it currently.
292+
293+
266294
## Kafka commands
267295

268296
If you've used `docker-compose` with the file `local/docker-compose.yml` during local setup to spawn kafka & zookeeper, you can use the following commands to manipulate kafka topics and messages:

app-constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ const Interviews = {
7575
RequestedForReschedule: 'Requested for reschedule',
7676
Rescheduled: 'Rescheduled',
7777
Completed: 'Completed',
78-
Cancelled: 'Cancelled'
78+
Cancelled: 'Cancelled',
79+
Expired: 'Expired'
7980
},
8081
// key: template name in x.ai, value: duration
8182
XaiTemplate: {

app.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,32 @@ app.use(cors({
2525
// Allow browsers access pagination data in headers
2626
exposedHeaders: ['X-Page', 'X-Per-Page', 'X-Total', 'X-Total-Pages', 'X-Prev-Page', 'X-Next-Page']
2727
}))
28-
app.use(express.json())
28+
app.use((...args) => {
29+
const [req, res, next] = args
30+
// For test nylas webhook, we need raw buffer
31+
// Here i sCustom Middleware to compute rawBody. Unfortunately using
32+
// JSON.stringify(req.body) will remove spaces and newlines, so verification
33+
// will fail. We must add this middleware to ensure we're computing the correct
34+
// signature
35+
if (req.path === `${config.BASE_PATH}/taas/nylas-webhooks`) {
36+
req.rawBody = ''
37+
req.on('data', (chunk) => (req.rawBody += chunk))
38+
req.on('error', () => res.status(500).send('Error parsing body'))
39+
40+
req.on('end', () => {
41+
// because the stream has been consumed, other parsers like bodyParser.json
42+
// cannot stream the request data and will time out so we must explicitly parse the body
43+
try {
44+
req.body = req.rawBody.length ? JSON.parse(req.rawBody) : {}
45+
next()
46+
} catch (err) {
47+
res.status(500).send('Error parsing body')
48+
}
49+
})
50+
return
51+
}
52+
return express.json()(...args)
53+
})
2954
app.use(express.urlencoded({ extended: true }))
3055
app.set('port', config.PORT)
3156

@@ -113,6 +138,9 @@ const server = app.listen(app.get('port'), () => {
113138
schedule.scheduleJob(config.CRON_INTERVIEW_COMPLETED, notificationSchedulerService.sendInterviewCompletedNotifications)
114139
schedule.scheduleJob(config.CRON_POST_INTERVIEW, notificationSchedulerService.sendPostInterviewActionNotifications)
115140
schedule.scheduleJob(config.CRON_UPCOMING_RESOURCE_BOOKING, notificationSchedulerService.sendResourceBookingExpirationNotifications)
141+
142+
schedule.scheduleJob(config.CRON_INTERVIEW_EXPIRED, notificationSchedulerService.sendInterviewExpiredNotifications)
143+
schedule.scheduleJob(config.CRON_INTERVIEW_SCHEDULE_REMINDER, notificationSchedulerService.sendInterviewScheduleReminderNotifications)
116144
})
117145

118146
if (process.env.NODE_ENV === 'test') {

config/default.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,7 @@ module.exports = {
178178
REPORT_ISSUE_SENDGRID_TEMPLATE_ID: process.env.REPORT_ISSUE_SENDGRID_TEMPLATE_ID,
179179
// SendGrid email template ID for requesting extension
180180
REQUEST_EXTENSION_SENDGRID_TEMPLATE_ID: process.env.REQUEST_EXTENSION_SENDGRID_TEMPLATE_ID,
181-
// SendGrid email template ID for interview invitation
182-
INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID: process.env.INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID,
183-
// The sender (aka `from`) email for invitation.
184-
INTERVIEW_INVITATION_SENDER_EMAIL: process.env.INTERVIEW_INVITATION_SENDER_EMAIL || '[email protected]',
181+
185182
// Duration that gets added to current time when an interview is created in order to calculate expireTimestamp
186183
INTERVIEW_SCHEDULING_EXPIRE_TIME: process.env.INTERVIEW_SCHEDULING_EXPIRE_TIME || 'P5D',
187184
// the URL where TaaS App is hosted
@@ -284,6 +281,14 @@ module.exports = {
284281
NOTIFICATION_INTERVIEWS_OVERLAPPING_SENDGRID_TEMPLATE_ID: process.env.NOTIFICATION_INTERVIEWS_OVERLAPPING_SENDGRID_TEMPLATE_ID,
285282
// the email notification sendgrid template id of job candidate selected
286283
NOTIFICATION_JOB_CANDIDATE_SELECTED_SENDGRID_TEMPLATE_ID: process.env.NOTIFICATION_JOB_CANDIDATE_SELECTED_SENDGRID_TEMPLATE_ID,
284+
// the email notification sendgrid template id of interview schedule reminder for job candidate
285+
NOTIFICATION_MEMBER_INTERVIEW_SCHEDULE_REMINDER_SENDGRID_TEMPLATE_ID: process.env.NOTIFICATION_MEMBER_INTERVIEW_SCHEDULE_REMINDER_SENDGRID_TEMPLATE_ID,
286+
// the email notification sendgrid template id of interview expired for customer
287+
NOTIFICATION_MEMBER_INTERVIEW_EXPIRED_SENDGRID_TEMPLATE_ID: process.env.NOTIFICATION_MEMBER_INTERVIEW_EXPIRED_SENDGRID_TEMPLATE_ID,
288+
// the email notification sendgrid template id of interview expired for member
289+
NOTIFICATION_CUSTOMER_INTERVIEW_EXPIRED_SENDGRID_TEMPLATE_ID: process.env.NOTIFICATION_CUSTOMER_INTERVIEW_EXPIRED_SENDGRID_TEMPLATE_ID,
290+
// the email notification sendgrid template id of job candidate invited
291+
NOTIFICATION_MEMBER_INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID: process.env.NOTIFICATION_MEMBER_INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID,
287292
// frequency of cron checking for available candidates for review
288293
CRON_CANDIDATE_REVIEW: process.env.CRON_CANDIDATE_REVIEW || '00 00 13 * * 0-6',
289294
// frequency of cron checking for coming up interviews
@@ -292,6 +297,10 @@ module.exports = {
292297
// frequency of cron checking for interview completed
293298
// when changing this to frequency other than 5 mins, please change the minutesRange in sendInterviewCompletedEmails correspondingly
294299
CRON_INTERVIEW_COMPLETED: process.env.CRON_INTERVIEW_COMPLETED || '*/5 * * * *',
300+
// frequency of checking expired interview
301+
CRON_INTERVIEW_EXPIRED: process.env.CRON_INTERVIEW_EXPIRED || '*/5 * * * *',
302+
// frequency of checking interview schedule status which need remind job candidate to select time
303+
CRON_INTERVIEW_SCHEDULE_REMINDER: process.env.CRON_INTERVIEW_SCHEDULE_REMINDER || '00 00 13 * * 0-6',
295304
// frequency of cron checking for post interview actions
296305
CRON_POST_INTERVIEW: process.env.CRON_POST_INTERVIEW || '00 00 13 * * 0-6',
297306
// frequency of cron checking for upcoming resource bookings
@@ -304,6 +313,10 @@ module.exports = {
304313
INTERVIEW_COMPLETED_MATCH_WINDOW: process.env.INTERVIEW_COMPLETED_MATCH_WINDOW || 'PT5M',
305314
// The interview completed past time for fetching interviews
306315
INTERVIEW_COMPLETED_PAST_TIME: process.env.INTERVIEW_COMPLETED_PAST_TIME || 'PT4H',
316+
// Reminder after days if Job Candidate hasn't selected time
317+
INTERVIEW_REMINDER_DAY_AFTER: process.env.INTERVIEW_REMINDER_DAY_AFTER || 'P1D',
318+
// Reminder frequency if Job Candidate hasn't selected time, unit: day
319+
INTERVIEW_REMINDER_FREQUENCY: parseInt(process.env.INTERVIEW_REMINDER_FREQUENCY) || 1,
307320
// The time before resource booking expiry when we should start sending notifications
308321
RESOURCE_BOOKING_EXPIRY_TIME: process.env.RESOURCE_BOOKING_EXPIRY_TIME || 'P21D',
309322
// The match window for fetching post interview actions

config/email_template.config.js

Lines changed: 29 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -63,48 +63,8 @@ module.exports = {
6363
'{{text}}',
6464
recipients: config.REPORT_ISSUE_EMAILS,
6565
sendgridTemplateId: config.REQUEST_EXTENSION_SENDGRID_TEMPLATE_ID
66-
},
67-
68-
/* Request interview for a job candidate
69-
*
70-
* - interviewType: the x.ai interview type. Example: "interview-30"
71-
* - interviewRound: the round of the interview. Example: 2
72-
* - interviewDuration: duration of the interview, in minutes. Example: 30
73-
* - interviewerList: The list of interviewer email addresses. Example: "[email protected], [email protected]"
74-
* - candidateId: the id of the jobCandidate. Example: "cc562545-7b75-48bf-87e7-50b3c57e41b1"
75-
* - candidateName: Full name of candidate. Example: "John Doe"
76-
* - jobName: The title of the job. Example: "TaaS API Misc Updates"
77-
*
78-
* Template (defined in SendGrid):
79-
* Subject: '{{interviewType}} tech interview with {{candidateName}} for {{jobName}} is requested by the Customer'
80-
* Body:
81-
* 'Hello!
82-
* <br /><br />
83-
* Congratulations, you have been selected to participate in a Topcoder Gig Work Interview!
84-
* <br /><br />
85-
* Please monitor your email for a response to this where you can coordinate your availability.
86-
* <br /><br />
87-
* Interviewee: {{candidateName}}<br />
88-
* Interviewer(s): {{interviewerList}}<br />
89-
* Interview Length: {{interviewDuration}} minutes
90-
* <br /><br />
91-
* /{{interviewType}}
92-
* <br /><br />
93-
* Topcoder Info:<br />
94-
* Note: "id: {{candidateId}}, round: {{interviewRound}}"'
95-
*
96-
* Note, that the template should be defined in SendGrid.
97-
* The subject & body above (identical to actual SendGrid template) is for reference purposes.
98-
* We won't pass subject & body but only substitutions (replacements in template subject/body).
99-
*/
100-
'interview-invitation': {
101-
subject: '',
102-
body: '',
103-
from: config.INTERVIEW_INVITATION_SENDER_EMAIL,
104-
cc: config.INTERVIEW_INVITATION_CC_LIST,
105-
recipients: config.INTERVIEW_INVITATION_RECIPIENTS_LIST,
106-
sendgridTemplateId: config.INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID
10766
}
67+
10868
},
10969

11070
/**
@@ -139,6 +99,34 @@ module.exports = {
13999
from: config.NOTIFICATION_SENDER_EMAIL,
140100
sendgridTemplateId: config.NOTIFICATION_MEMBER_INTERVIEW_COMING_UP_SENDGRID_TEMPLATE_ID
141101
},
102+
'taas.notification.interview-invitation': {
103+
subject: 'Please select your available time',
104+
body: '',
105+
recipients: [],
106+
from: config.NOTIFICATION_SENDER_EMAIL,
107+
sendgridTemplateId: config.NOTIFICATION_MEMBER_INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID
108+
},
109+
'taas.notification.interview-expired-host': {
110+
subject: 'Your interview is expired',
111+
body: '',
112+
recipients: [],
113+
from: config.NOTIFICATION_SENDER_EMAIL,
114+
sendgridTemplateId: config.NOTIFICATION_CUSTOMER_INTERVIEW_EXPIRED_SENDGRID_TEMPLATE_ID
115+
},
116+
'taas.notification.interview-expired-guest': {
117+
subject: 'Interview expired - your candidate didn\'t select time',
118+
body: '',
119+
recipients: [],
120+
from: config.NOTIFICATION_SENDER_EMAIL,
121+
sendgridTemplateId: config.NOTIFICATION_MEMBER_INTERVIEW_EXPIRED_SENDGRID_TEMPLATE_ID
122+
},
123+
'taas.notification.interview-schedule-reminder': {
124+
subject: 'Reminder: Please select your available time for interview',
125+
body: '',
126+
recipients: [],
127+
from: config.NOTIFICATION_SENDER_EMAIL,
128+
sendgridTemplateId: config.NOTIFICATION_MEMBER_INTERVIEW_SCHEDULE_REMINDER_SENDGRID_TEMPLATE_ID
129+
},
142130
'taas.notification.interview-awaits-resolution': {
143131
subject: 'Interview complete - here’s what to do next',
144132
body: '',

data/demo-data.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,32 @@
10061006
"updatedAt": "2021-05-09T21:21:22.428Z",
10071007
"deletedAt": null
10081008
},
1009+
{
1010+
"id": "505db942-79fe-4b6f-974c-b359e1b61967",
1011+
"jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36",
1012+
"nylasPageId": "4aae1d32-1d35-4320-be74-82a73bf032fa",
1013+
"availableTime": [
1014+
{
1015+
"start": "09:00",
1016+
"end": "17:00",
1017+
"days": ["M", "W"]
1018+
}
1019+
],
1020+
"nylasCalendarId": "785735",
1021+
"timezone": "Europe/London",
1022+
"hostUserId": "f3e3a400-44e9-445a-96b4-adff39b0344d",
1023+
"nylasPageSlug": "tc-taas-interview-505db942-79fe-4b6f-974c-b359e1b61967",
1024+
"duration": null,
1025+
"round": 2,
1026+
"startTimestamp": null,
1027+
"endTimestamp": null,
1028+
"status": "Scheduling",
1029+
"createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c",
1030+
"updatedBy": null,
1031+
"createdAt": "2021-05-09T21:21:22.428Z",
1032+
"updatedAt": "2021-05-09T21:21:22.428Z",
1033+
"deletedAt": null
1034+
},
10091035
{
10101036
"id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb",
10111037
"jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36",

0 commit comments

Comments
 (0)