Skip to content

Commit a336c1d

Browse files
authored
Merge pull request topcoder-platform#625 from topcoder-platform/feature/interview-update-part-1
Fix specific issues related to Nylas webhooks & interviews - part 1
2 parents 6c25130 + 7c5fe2b commit a336c1d

File tree

4 files changed

+99
-53
lines changed

4 files changed

+99
-53
lines changed

docs/swagger.yaml

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4859,11 +4859,7 @@ components:
48594859
type: string
48604860
format: uuid
48614861

4862-
UpdateInterviewRequestBody:
4863-
required:
4864-
- duration
4865-
- timezone
4866-
- availableTime
4862+
UpdateInterviewByRequestBody:
48674863
properties:
48684864
duration:
48694865
type: integer
@@ -4872,6 +4868,13 @@ components:
48724868
timezone:
48734869
type: string
48744870
example: "Europe/London"
4871+
hostUserId:
4872+
type: string
4873+
description: "UUID of interview host user"
4874+
expireTimestamp:
4875+
type: string
4876+
format: date-time
4877+
description: "Interview expiry time stamp."
48754878
availableTime:
48764879
type: array
48774880
items:
@@ -4894,6 +4897,20 @@ components:
48944897
type: string
48954898
example: "10:00"
48964899
pattern: "^[0-9]{1,2}:[0-9]{2}$"
4900+
startTimestamp:
4901+
type: string
4902+
format: date-time
4903+
description: "Interview start time."
4904+
endTimestamp:
4905+
type: string
4906+
format: date-time
4907+
description: "Interview end time."
4908+
status:
4909+
type: string
4910+
enum: ["Scheduling", "Scheduled", "Requested for reschedule", "Rescheduled", "Completed", "Cancelled", "Expired"]
4911+
deletedAt:
4912+
type: string
4913+
format: date-time
48974914

48984915
JobPatchRequestBody:
48994916
properties:

src/services/InterviewService.js

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -467,8 +467,8 @@ partiallyUpdateInterviewByRound.schema = Joi.object().keys({
467467
jobCandidateId: Joi.string().uuid().required(),
468468
round: Joi.number().integer().positive().required(),
469469
data: Joi.object().keys({
470-
duration: Joi.number().integer().positive().required(),
471-
timezone: Joi.string().required(),
470+
duration: Joi.number().integer().positive(),
471+
timezone: Joi.string(),
472472
hostUserId: Joi.string().uuid(),
473473
expireTimestamp: Joi.date(),
474474
availableTime: Joi.array().min(1).items(
@@ -484,15 +484,15 @@ partiallyUpdateInterviewByRound.schema = Joi.object().keys({
484484
end: Joi.string().regex(InterviewConstants.Nylas.StartEndRegex).required(),
485485
start: Joi.string().regex(InterviewConstants.Nylas.StartEndRegex).required()
486486
})
487-
).required(),
487+
),
488488
startTimestamp: Joi.date().greater('now').when('status', {
489489
is: [InterviewConstants.Status.Scheduled, InterviewConstants.Status.Rescheduled],
490-
then: Joi.required(),
490+
then: Joi.invalid(null),
491491
otherwise: Joi.allow(null)
492492
}),
493493
endTimestamp: Joi.date().greater(Joi.ref('startTimestamp')).when('status', {
494494
is: [InterviewConstants.Status.Scheduled, InterviewConstants.Status.Rescheduled],
495-
then: Joi.required(),
495+
then: Joi.invalid(null),
496496
otherwise: Joi.allow(null)
497497
}),
498498
status: Joi.interviewStatus(),
@@ -540,8 +540,8 @@ partiallyUpdateInterviewById.schema = Joi.object().keys({
540540
currentUser: Joi.object().required(),
541541
id: Joi.string().required(),
542542
data: Joi.object().keys({
543-
duration: Joi.number().integer().positive().required(),
544-
timezone: Joi.string().required(),
543+
duration: Joi.number().integer().positive(),
544+
timezone: Joi.string(),
545545
hostUserId: Joi.string().uuid(),
546546
expireTimestamp: Joi.date(),
547547
availableTime: Joi.array().min(1).items(
@@ -557,15 +557,15 @@ partiallyUpdateInterviewById.schema = Joi.object().keys({
557557
end: Joi.string().regex(InterviewConstants.Nylas.StartEndRegex).required(),
558558
start: Joi.string().regex(InterviewConstants.Nylas.StartEndRegex).required()
559559
})
560-
).required(),
560+
),
561561
startTimestamp: Joi.date().greater('now').when('status', {
562562
is: [InterviewConstants.Status.Scheduled, InterviewConstants.Status.Rescheduled],
563-
then: Joi.required(),
563+
then: Joi.invalid(null),
564564
otherwise: Joi.allow(null)
565565
}),
566566
endTimestamp: Joi.date().greater(Joi.ref('startTimestamp')).when('status', {
567567
is: [InterviewConstants.Status.Scheduled, InterviewConstants.Status.Rescheduled],
568-
then: Joi.required(),
568+
then: Joi.invalid(null),
569569
otherwise: Joi.allow(null)
570570
}),
571571
status: Joi.interviewStatus(),
@@ -767,6 +767,47 @@ async function updateCompletedInterviews () {
767767
*/
768768
async function partiallyUpdateInterviewByWebhook (interviewId, webhookBody) {
769769
logger.info({ component: 'InterviewService', context: 'partiallyUpdateInterviewByWebhook', message: `Received webhook for interview id "${interviewId}": ${JSON.stringify(webhookBody)}` })
770+
771+
// this method is used by the Nylas webhooks, so use M2M user
772+
const m2mUser = helper.getAuditM2Muser()
773+
const bookingDetails = webhookBody.booking
774+
const interviewStartTimeMoment = moment.unix(bookingDetails.start_time)
775+
const interviewEndTimeMoment = moment.unix(bookingDetails.end_time)
776+
let updatedInterview
777+
778+
if (bookingDetails.is_confirmed) {
779+
try {
780+
// CREATED + confirmed ==> inteview updated to scheduled
781+
// UPDATED + cancelled ==> inteview expired
782+
updatedInterview = await partiallyUpdateInterviewById(
783+
m2mUser,
784+
interviewId,
785+
{
786+
status: InterviewConstants.Status.Scheduled,
787+
startTimestamp: interviewStartTimeMoment.toDate(),
788+
endTimestamp: interviewEndTimeMoment.toDate()
789+
}
790+
)
791+
792+
logger.debug({
793+
component: 'InterviewService',
794+
context: 'partiallyUpdateInterviewByWebhook',
795+
message:
796+
`~~~~~~~~~~~NEW EVENT~~~~~~~~~~~\nInterview Scheduled under account id ${
797+
bookingDetails.account_id
798+
} (email is ${bookingDetails.recipient_email}) in calendar id ${
799+
bookingDetails.calendar_id
800+
}. Event status is ${InterviewConstants.Status.Scheduled} and starts from ${interviewStartTimeMoment
801+
.format('MMM DD YYYY HH:mm')} and ends at ${interviewEndTimeMoment
802+
.format('MMM DD YYYY HH:mm')}`
803+
})
804+
} catch (err) {
805+
logger.logFullError(err, { component: 'InterviewService', context: 'partiallyUpdateInterviewByWebhook' })
806+
throw new errors.BadRequestError(`Could not update interview: ${err.message}`)
807+
}
808+
809+
return updatedInterview
810+
}
770811
}
771812

772813
module.exports = {

src/services/NotificationsSchedulerService.js

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ const ResourceBooking = models.ResourceBooking
1313
const helper = require('../common/helper')
1414
const constants = require('../../app-constants')
1515
const logger = require('../common/logger')
16-
const { processUpdateInterview } = require('../esProcessors/InterviewProcessor')
16+
const { getAuditM2Muser } = require('../common/helper')
17+
const interviewService = require('./InterviewService')
1718

1819
const localLogger = {
1920
debug: (message, context) => logger.debug({ component: 'NotificationSchedulerService', context, message }),
@@ -655,17 +656,6 @@ async function sendInterviewScheduleReminderNotifications () {
655656
localLogger.debug(`[sendInterviewScheduleReminderNotifications]: Sent notifications for ${interviewCount} interviews which need to schedule.`)
656657
}
657658

658-
/**
659-
* Update interview status by id and jobCandidateId
660-
* @param {*} entity interview entity
661-
*/
662-
async function updateInterviewStatus (entity) {
663-
const interviewOldValue = await Interview.findById(entity.id)
664-
await Interview.update({ status: entity.status }, { where: { id: entity.id } })
665-
await processUpdateInterview(entity)
666-
await helper.postEvent(config.TAAS_INTERVIEW_UPDATE_TOPIC, entity, { oldValue: interviewOldValue.toJSON() })
667-
}
668-
669659
// Send notifications to customer and candidate this interview has expired
670660
async function sendInterviewExpiredNotifications () {
671661
localLogger.debug('[sendInterviewExpiredNotifications]: Looking for due records...')
@@ -705,7 +695,17 @@ async function sendInterviewExpiredNotifications () {
705695
const templateGuest = 'taas.notification.interview-expired-guest'
706696

707697
for (const interview of interviews) {
708-
await updateInterviewStatus({ status: constants.Interviews.Status.Expired, id: interview.id, jobCandidateId: interview.jobCandidateId })
698+
// this method is run by the app itself
699+
const m2mUser = getAuditM2Muser()
700+
701+
await interviewService.partiallyUpdateInterviewById(
702+
m2mUser,
703+
interview.id,
704+
{
705+
status: constants.Interviews.Status.Expired
706+
}
707+
)
708+
709709
// send host email
710710
const data = await getDataForInterview(interview)
711711
if (!data) { continue }
@@ -762,7 +762,6 @@ module.exports = {
762762
sendInterviewCompletedNotifications: errorCatchWrapper(sendInterviewCompletedNotifications, 'sendInterviewCompletedNotifications'),
763763
sendInterviewExpiredNotifications: errorCatchWrapper(sendInterviewExpiredNotifications, 'sendInterviewExpiredNotifications'),
764764
sendInterviewScheduleReminderNotifications: errorCatchWrapper(sendInterviewScheduleReminderNotifications, 'sendInterviewScheduleReminderNotifications'),
765-
updateInterviewStatus,
766765
getDataForInterview,
767766
sendPostInterviewActionNotifications: errorCatchWrapper(sendPostInterviewActionNotifications, 'sendPostInterviewActionNotifications'),
768767
sendResourceBookingExpirationNotifications: errorCatchWrapper(sendResourceBookingExpirationNotifications, 'sendResourceBookingExpirationNotifications')

src/services/NylasWebhookService.js

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const axios = require('axios')
1010
const moment = require('moment')
1111

1212
const logger = require('../common/logger')
13-
const { updateInterviewStatus } = require('./NotificationsSchedulerService')
13+
const { partiallyUpdateInterviewById } = require('./InterviewService')
14+
const { getAuditM2Muser } = require('../common/helper')
1415

1516
const localLogger = {
1617
debug: (message, context) =>
@@ -123,34 +124,22 @@ async function processFormattedEvent (webhookData, event) {
123124
}
124125
})
125126

126-
if (webhookData.type === EVENTTYPES.CREATED && event.status === 'confirmed') {
127-
// CREATED + confirmed ==> inteview updated to scheduled
128-
// UPDATED + cancelled ==> inteview expired
129-
130-
await updateInterviewStatus({
131-
status: constants.Interviews.Status.Scheduled,
132-
startTimestamp: moment.unix(event.startTime).toDate(),
133-
endTimestamp: moment.unix(event.endTime).toDate(),
134-
id: interview.id,
135-
jobCandidateId: interview.jobCandidateId
136-
})
127+
// this method is used by the Nylas webhooks, so use M2M user
128+
const m2mUser = getAuditM2Muser()
137129

138-
localLogger.debug(
139-
`~~~~~~~~~~~NEW EVENT~~~~~~~~~~~\nInterview Scheduled under account id ${
140-
event.accountId
141-
} (email is ${event.email}) in calendar id ${
142-
event.calendarId
143-
}. Event status is ${event.status} and starts from ${moment
144-
.unix(event.startTime)
145-
.format('MMM DD YYYY HH:mm')} and ends at ${moment
146-
.unix(event.endTime)
147-
.format('MMM DD YYYY HH:mm')}`
148-
)
130+
if (webhookData.type === EVENTTYPES.CREATED && event.status === 'confirmed') {
131+
localLogger.info('~~~~~~~~~~~NEW EVENT~~~~~~~~~~~\nEvent "Interview Scheduled" being processed by method InterviewService.partiallyUpdateInterviewByWebhook')
149132
} else if (
150133
webhookData.type === EVENTTYPES.UPDATED &&
151134
event.status === 'cancelled'
152135
) {
153-
await updateInterviewStatus({ status: constants.Interviews.Status.Cancelled, id: interview.id, jobCandidateId: interview.jobCandidateId })
136+
await partiallyUpdateInterviewById(
137+
m2mUser,
138+
interview.id,
139+
{
140+
status: constants.Interviews.Status.Cancelled
141+
}
142+
)
154143

155144
localLogger.debug(
156145
`~~~~~~~~~~~NEW EVENT~~~~~~~~~~~\nInterview cancelled under account id ${

0 commit comments

Comments
 (0)