Skip to content

Feature/interview nylas #571

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@
AUTH0_AUDIENCE_UBAHN=
AUTH0_CLIENT_ID=
AUTH0_CLIENT_SECRET=
# necessary if you'll utilize email functionality of interviews
INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID=
INTERVIEW_INVITATION_SENDER_EMAIL=
# Get nylas client id and secret from nylas developer page
NYLAS_CLIENT_ID=
NYLAS_CLIENT_SECRET=
# Locally deployed services (via docker-compose)
ES_HOST=http://dockerhost:9200
DATABASE_URL=postgres://postgres:postgres@dockerhost:5432/postgres
Expand Down Expand Up @@ -263,6 +263,34 @@ npm run data:import -- --file path/to-file.json

- List of models that will be imported are defined in `scripts/data/importData.js`.

## Nylas Webhook verification

### schedule
ngrok http 3000

in https://dashboard.nylas.com/applications/{id} create nylas webhook url with created/updated/deleted event trigger
```
https://{generatedId}.ngrok.io/api/v5/taas/nylas-webhooks
```


create job, job candidate, and request interview

You will get a invitation demo email in out/Please select your available time-xxxx.html, open it in broswer

go to `https://schedule.nylas.com/{nylasSlug}` to schedule a time.

In Postman, invoke `Get interview by id` to see the interview status

### cancel
In your email, click `cancel` to cancel interview

Verify inteview status by postman

### reschedule
The webhook didn't support it(schedule and reschedule have same event), ignore it currently.


## Kafka commands

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:
Expand Down
3 changes: 2 additions & 1 deletion app-constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ const Interviews = {
RequestedForReschedule: 'Requested for reschedule',
Rescheduled: 'Rescheduled',
Completed: 'Completed',
Cancelled: 'Cancelled'
Cancelled: 'Cancelled',
Expired: 'Expired'
},
// key: template name in x.ai, value: duration
XaiTemplate: {
Expand Down
30 changes: 29 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,32 @@ app.use(cors({
// Allow browsers access pagination data in headers
exposedHeaders: ['X-Page', 'X-Per-Page', 'X-Total', 'X-Total-Pages', 'X-Prev-Page', 'X-Next-Page']
}))
app.use(express.json())
app.use((...args) => {
const [req, res, next] = args
// For test nylas webhook, we need raw buffer
// Here i sCustom Middleware to compute rawBody. Unfortunately using
// JSON.stringify(req.body) will remove spaces and newlines, so verification
// will fail. We must add this middleware to ensure we're computing the correct
// signature
if (req.path === `${config.BASE_PATH}/taas/nylas-webhooks`) {
req.rawBody = ''
req.on('data', (chunk) => (req.rawBody += chunk))
req.on('error', () => res.status(500).send('Error parsing body'))

req.on('end', () => {
// because the stream has been consumed, other parsers like bodyParser.json
// cannot stream the request data and will time out so we must explicitly parse the body
try {
req.body = req.rawBody.length ? JSON.parse(req.rawBody) : {}
next()
} catch (err) {
res.status(500).send('Error parsing body')
}
})
return
}
return express.json()(...args)
})
app.use(express.urlencoded({ extended: true }))
app.set('port', config.PORT)

Expand Down Expand Up @@ -113,6 +138,9 @@ const server = app.listen(app.get('port'), () => {
schedule.scheduleJob(config.CRON_INTERVIEW_COMPLETED, notificationSchedulerService.sendInterviewCompletedNotifications)
schedule.scheduleJob(config.CRON_POST_INTERVIEW, notificationSchedulerService.sendPostInterviewActionNotifications)
schedule.scheduleJob(config.CRON_UPCOMING_RESOURCE_BOOKING, notificationSchedulerService.sendResourceBookingExpirationNotifications)

schedule.scheduleJob(config.CRON_INTERVIEW_EXPIRED, notificationSchedulerService.sendInterviewExpiredNotifications)
schedule.scheduleJob(config.CRON_INTERVIEW_SCHEDULE_REMINDER, notificationSchedulerService.sendInterviewScheduleReminderNotifications)
})

if (process.env.NODE_ENV === 'test') {
Expand Down
21 changes: 17 additions & 4 deletions config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,7 @@ module.exports = {
REPORT_ISSUE_SENDGRID_TEMPLATE_ID: process.env.REPORT_ISSUE_SENDGRID_TEMPLATE_ID,
// SendGrid email template ID for requesting extension
REQUEST_EXTENSION_SENDGRID_TEMPLATE_ID: process.env.REQUEST_EXTENSION_SENDGRID_TEMPLATE_ID,
// SendGrid email template ID for interview invitation
INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID: process.env.INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID,
// The sender (aka `from`) email for invitation.
INTERVIEW_INVITATION_SENDER_EMAIL: process.env.INTERVIEW_INVITATION_SENDER_EMAIL || '[email protected]',

// Duration that gets added to current time when an interview is created in order to calculate expireTimestamp
INTERVIEW_SCHEDULING_EXPIRE_TIME: process.env.INTERVIEW_SCHEDULING_EXPIRE_TIME || 'P5D',
// the URL where TaaS App is hosted
Expand Down Expand Up @@ -284,6 +281,14 @@ module.exports = {
NOTIFICATION_INTERVIEWS_OVERLAPPING_SENDGRID_TEMPLATE_ID: process.env.NOTIFICATION_INTERVIEWS_OVERLAPPING_SENDGRID_TEMPLATE_ID,
// the email notification sendgrid template id of job candidate selected
NOTIFICATION_JOB_CANDIDATE_SELECTED_SENDGRID_TEMPLATE_ID: process.env.NOTIFICATION_JOB_CANDIDATE_SELECTED_SENDGRID_TEMPLATE_ID,
// the email notification sendgrid template id of interview schedule reminder for job candidate
NOTIFICATION_MEMBER_INTERVIEW_SCHEDULE_REMINDER_SENDGRID_TEMPLATE_ID: process.env.NOTIFICATION_MEMBER_INTERVIEW_SCHEDULE_REMINDER_SENDGRID_TEMPLATE_ID,
// the email notification sendgrid template id of interview expired for customer
NOTIFICATION_MEMBER_INTERVIEW_EXPIRED_SENDGRID_TEMPLATE_ID: process.env.NOTIFICATION_MEMBER_INTERVIEW_EXPIRED_SENDGRID_TEMPLATE_ID,
// the email notification sendgrid template id of interview expired for member
NOTIFICATION_CUSTOMER_INTERVIEW_EXPIRED_SENDGRID_TEMPLATE_ID: process.env.NOTIFICATION_CUSTOMER_INTERVIEW_EXPIRED_SENDGRID_TEMPLATE_ID,
// the email notification sendgrid template id of job candidate invited
NOTIFICATION_MEMBER_INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID: process.env.NOTIFICATION_MEMBER_INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID,
// frequency of cron checking for available candidates for review
CRON_CANDIDATE_REVIEW: process.env.CRON_CANDIDATE_REVIEW || '00 00 13 * * 0-6',
// frequency of cron checking for coming up interviews
Expand All @@ -292,6 +297,10 @@ module.exports = {
// frequency of cron checking for interview completed
// when changing this to frequency other than 5 mins, please change the minutesRange in sendInterviewCompletedEmails correspondingly
CRON_INTERVIEW_COMPLETED: process.env.CRON_INTERVIEW_COMPLETED || '*/5 * * * *',
// frequency of checking expired interview
CRON_INTERVIEW_EXPIRED: process.env.CRON_INTERVIEW_EXPIRED || '*/5 * * * *',
// frequency of checking interview schedule status which need remind job candidate to select time
CRON_INTERVIEW_SCHEDULE_REMINDER: process.env.CRON_INTERVIEW_SCHEDULE_REMINDER || '00 00 13 * * 0-6',
// frequency of cron checking for post interview actions
CRON_POST_INTERVIEW: process.env.CRON_POST_INTERVIEW || '00 00 13 * * 0-6',
// frequency of cron checking for upcoming resource bookings
Expand All @@ -304,6 +313,10 @@ module.exports = {
INTERVIEW_COMPLETED_MATCH_WINDOW: process.env.INTERVIEW_COMPLETED_MATCH_WINDOW || 'PT5M',
// The interview completed past time for fetching interviews
INTERVIEW_COMPLETED_PAST_TIME: process.env.INTERVIEW_COMPLETED_PAST_TIME || 'PT4H',
// Reminder after days if Job Candidate hasn't selected time
INTERVIEW_REMINDER_DAY_AFTER: process.env.INTERVIEW_REMINDER_DAY_AFTER || 'P1D',
// Reminder frequency if Job Candidate hasn't selected time, unit: day
INTERVIEW_REMINDER_FREQUENCY: parseInt(process.env.INTERVIEW_REMINDER_FREQUENCY) || 1,
// The time before resource booking expiry when we should start sending notifications
RESOURCE_BOOKING_EXPIRY_TIME: process.env.RESOURCE_BOOKING_EXPIRY_TIME || 'P21D',
// The match window for fetching post interview actions
Expand Down
70 changes: 29 additions & 41 deletions config/email_template.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,48 +63,8 @@ module.exports = {
'{{text}}',
recipients: config.REPORT_ISSUE_EMAILS,
sendgridTemplateId: config.REQUEST_EXTENSION_SENDGRID_TEMPLATE_ID
},

/* Request interview for a job candidate
*
* - interviewType: the x.ai interview type. Example: "interview-30"
* - interviewRound: the round of the interview. Example: 2
* - interviewDuration: duration of the interview, in minutes. Example: 30
* - interviewerList: The list of interviewer email addresses. Example: "[email protected], [email protected]"
* - candidateId: the id of the jobCandidate. Example: "cc562545-7b75-48bf-87e7-50b3c57e41b1"
* - candidateName: Full name of candidate. Example: "John Doe"
* - jobName: The title of the job. Example: "TaaS API Misc Updates"
*
* Template (defined in SendGrid):
* Subject: '{{interviewType}} tech interview with {{candidateName}} for {{jobName}} is requested by the Customer'
* Body:
* 'Hello!
* <br /><br />
* Congratulations, you have been selected to participate in a Topcoder Gig Work Interview!
* <br /><br />
* Please monitor your email for a response to this where you can coordinate your availability.
* <br /><br />
* Interviewee: {{candidateName}}<br />
* Interviewer(s): {{interviewerList}}<br />
* Interview Length: {{interviewDuration}} minutes
* <br /><br />
* /{{interviewType}}
* <br /><br />
* Topcoder Info:<br />
* Note: "id: {{candidateId}}, round: {{interviewRound}}"'
*
* Note, that the template should be defined in SendGrid.
* The subject & body above (identical to actual SendGrid template) is for reference purposes.
* We won't pass subject & body but only substitutions (replacements in template subject/body).
*/
'interview-invitation': {
subject: '',
body: '',
from: config.INTERVIEW_INVITATION_SENDER_EMAIL,
cc: config.INTERVIEW_INVITATION_CC_LIST,
recipients: config.INTERVIEW_INVITATION_RECIPIENTS_LIST,
sendgridTemplateId: config.INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID
}

},

/**
Expand Down Expand Up @@ -139,6 +99,34 @@ module.exports = {
from: config.NOTIFICATION_SENDER_EMAIL,
sendgridTemplateId: config.NOTIFICATION_MEMBER_INTERVIEW_COMING_UP_SENDGRID_TEMPLATE_ID
},
'taas.notification.interview-invitation': {
subject: 'Please select your available time',
body: '',
recipients: [],
from: config.NOTIFICATION_SENDER_EMAIL,
sendgridTemplateId: config.NOTIFICATION_MEMBER_INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID
},
'taas.notification.interview-expired-host': {
subject: 'Your interview is expired',
body: '',
recipients: [],
from: config.NOTIFICATION_SENDER_EMAIL,
sendgridTemplateId: config.NOTIFICATION_CUSTOMER_INTERVIEW_EXPIRED_SENDGRID_TEMPLATE_ID
},
'taas.notification.interview-expired-guest': {
subject: 'Interview expired - your candidate didn\'t select time',
body: '',
recipients: [],
from: config.NOTIFICATION_SENDER_EMAIL,
sendgridTemplateId: config.NOTIFICATION_MEMBER_INTERVIEW_EXPIRED_SENDGRID_TEMPLATE_ID
},
'taas.notification.interview-schedule-reminder': {
subject: 'Reminder: Please select your available time for interview',
body: '',
recipients: [],
from: config.NOTIFICATION_SENDER_EMAIL,
sendgridTemplateId: config.NOTIFICATION_MEMBER_INTERVIEW_SCHEDULE_REMINDER_SENDGRID_TEMPLATE_ID
},
'taas.notification.interview-awaits-resolution': {
subject: 'Interview complete - here’s what to do next',
body: '',
Expand Down
26 changes: 26 additions & 0 deletions data/demo-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,32 @@
"updatedAt": "2021-05-09T21:21:22.428Z",
"deletedAt": null
},
{
"id": "505db942-79fe-4b6f-974c-b359e1b61967",
"jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36",
"nylasPageId": "4aae1d32-1d35-4320-be74-82a73bf032fa",
"availableTime": [
{
"start": "09:00",
"end": "17:00",
"days": ["M", "W"]
}
],
"nylasCalendarId": "785735",
"timezone": "Europe/London",
"hostUserId": "f3e3a400-44e9-445a-96b4-adff39b0344d",
"nylasPageSlug": "tc-taas-interview-505db942-79fe-4b6f-974c-b359e1b61967",
"duration": null,
"round": 2,
"startTimestamp": null,
"endTimestamp": null,
"status": "Scheduling",
"createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c",
"updatedBy": null,
"createdAt": "2021-05-09T21:21:22.428Z",
"updatedAt": "2021-05-09T21:21:22.428Z",
"deletedAt": null
},
{
"id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb",
"jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36",
Expand Down
Loading