Skip to content

Commit 52e3f84

Browse files
authored
Merge branch 'dev' into issue-560
2 parents 0d83b81 + 5af2883 commit 52e3f84

File tree

5 files changed

+103
-65
lines changed

5 files changed

+103
-65
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:

scripts/demo-email-notifications/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ async function initConsumer () {
9898
if (message.payload.notifications) {
9999
_.forEach(_.filter(message.payload.notifications, ['serviceId', 'email']), (notification) => {
100100
const email = templateFileMap[notification.details.sendgridTemplateId](notification.details.data)
101-
fs.writeFileSync(`./out/${notification.details.data.subject}-${Date.now()}.html`, email)
101+
fs.writeFileSync(`./out/${notification.details.data.subject.replace(/[^a-z0-9 ]/gi, '_')}-${Date.now()}.html`, email)
102102
})
103103
for (const notification of _.filter(message.payload.notifications, ['serviceId', 'slack'])) {
104104
if (process.env.SLACK_WEBHOOK_URL) {

src/services/InterviewService.js

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ async function requestInterview (currentUser, jobCandidateId, interview) {
299299
}
300300
// create scheduling page on nylas
301301
const schedulingPage = await createSchedulingPage(interview, calendar, pageOptions)
302+
logger.debug(`requestInterview -> createSchedulingPage created: ${JSON.stringify(schedulingPage)}, using accessToken: "${calendar.accessToken}""`)
302303

303304
// Link nylasPage to interview
304305
interview.nylasPageId = schedulingPage.id
@@ -386,6 +387,7 @@ requestInterview.schema = Joi.object().keys({
386387
* @returns {Object} updated interview
387388
*/
388389
async function partiallyUpdateInterview (currentUser, interview, data) {
390+
const oldInterviewValue = interview.toJSON()
389391
// only status can be updated for Completed interviews
390392
if (interview.status === InterviewConstants.Status.Completed) {
391393
const updatedFields = _.keys(data)
@@ -434,7 +436,7 @@ async function partiallyUpdateInterview (currentUser, interview, data) {
434436
// if reaches here, it's not one of the common errors handled in `handleSequelizeError`
435437
throw err
436438
}
437-
await helper.postEvent(config.TAAS_INTERVIEW_UPDATE_TOPIC, entity, { oldValue: interview.toJSON() })
439+
await helper.postEvent(config.TAAS_INTERVIEW_UPDATE_TOPIC, entity, { oldValue: oldInterviewValue })
438440
return entity
439441
}
440442

@@ -467,8 +469,8 @@ partiallyUpdateInterviewByRound.schema = Joi.object().keys({
467469
jobCandidateId: Joi.string().uuid().required(),
468470
round: Joi.number().integer().positive().required(),
469471
data: Joi.object().keys({
470-
duration: Joi.number().integer().positive().required(),
471-
timezone: Joi.string().required(),
472+
duration: Joi.number().integer().positive(),
473+
timezone: Joi.string(),
472474
hostUserId: Joi.string().uuid(),
473475
expireTimestamp: Joi.date(),
474476
availableTime: Joi.array().min(1).items(
@@ -484,15 +486,15 @@ partiallyUpdateInterviewByRound.schema = Joi.object().keys({
484486
end: Joi.string().regex(InterviewConstants.Nylas.StartEndRegex).required(),
485487
start: Joi.string().regex(InterviewConstants.Nylas.StartEndRegex).required()
486488
})
487-
).required(),
489+
),
488490
startTimestamp: Joi.date().greater('now').when('status', {
489491
is: [InterviewConstants.Status.Scheduled, InterviewConstants.Status.Rescheduled],
490-
then: Joi.required(),
492+
then: Joi.invalid(null),
491493
otherwise: Joi.allow(null)
492494
}),
493495
endTimestamp: Joi.date().greater(Joi.ref('startTimestamp')).when('status', {
494496
is: [InterviewConstants.Status.Scheduled, InterviewConstants.Status.Rescheduled],
495-
then: Joi.required(),
497+
then: Joi.invalid(null),
496498
otherwise: Joi.allow(null)
497499
}),
498500
status: Joi.interviewStatus(),
@@ -540,8 +542,8 @@ partiallyUpdateInterviewById.schema = Joi.object().keys({
540542
currentUser: Joi.object().required(),
541543
id: Joi.string().required(),
542544
data: Joi.object().keys({
543-
duration: Joi.number().integer().positive().required(),
544-
timezone: Joi.string().required(),
545+
duration: Joi.number().integer().positive(),
546+
timezone: Joi.string(),
545547
hostUserId: Joi.string().uuid(),
546548
expireTimestamp: Joi.date(),
547549
availableTime: Joi.array().min(1).items(
@@ -557,15 +559,15 @@ partiallyUpdateInterviewById.schema = Joi.object().keys({
557559
end: Joi.string().regex(InterviewConstants.Nylas.StartEndRegex).required(),
558560
start: Joi.string().regex(InterviewConstants.Nylas.StartEndRegex).required()
559561
})
560-
).required(),
562+
),
561563
startTimestamp: Joi.date().greater('now').when('status', {
562564
is: [InterviewConstants.Status.Scheduled, InterviewConstants.Status.Rescheduled],
563-
then: Joi.required(),
565+
then: Joi.invalid(null),
564566
otherwise: Joi.allow(null)
565567
}),
566568
endTimestamp: Joi.date().greater(Joi.ref('startTimestamp')).when('status', {
567569
is: [InterviewConstants.Status.Scheduled, InterviewConstants.Status.Rescheduled],
568-
then: Joi.required(),
570+
then: Joi.invalid(null),
569571
otherwise: Joi.allow(null)
570572
}),
571573
status: Joi.interviewStatus(),
@@ -767,6 +769,47 @@ async function updateCompletedInterviews () {
767769
*/
768770
async function partiallyUpdateInterviewByWebhook (interviewId, webhookBody) {
769771
logger.info({ component: 'InterviewService', context: 'partiallyUpdateInterviewByWebhook', message: `Received webhook for interview id "${interviewId}": ${JSON.stringify(webhookBody)}` })
772+
773+
// this method is used by the Nylas webhooks, so use M2M user
774+
const m2mUser = helper.getAuditM2Muser()
775+
const bookingDetails = webhookBody.booking
776+
const interviewStartTimeMoment = moment.unix(bookingDetails.start_time)
777+
const interviewEndTimeMoment = moment.unix(bookingDetails.end_time)
778+
let updatedInterview
779+
780+
if (bookingDetails.is_confirmed) {
781+
try {
782+
// CREATED + confirmed ==> inteview updated to scheduled
783+
// UPDATED + cancelled ==> inteview expired
784+
updatedInterview = await partiallyUpdateInterviewById(
785+
m2mUser,
786+
interviewId,
787+
{
788+
status: InterviewConstants.Status.Scheduled,
789+
startTimestamp: interviewStartTimeMoment.toDate(),
790+
endTimestamp: interviewEndTimeMoment.toDate()
791+
}
792+
)
793+
794+
logger.debug({
795+
component: 'InterviewService',
796+
context: 'partiallyUpdateInterviewByWebhook',
797+
message:
798+
`~~~~~~~~~~~NEW EVENT~~~~~~~~~~~\nInterview Scheduled under account id ${
799+
bookingDetails.account_id
800+
} (email is ${bookingDetails.recipient_email}) in calendar id ${
801+
bookingDetails.calendar_id
802+
}. Event status is ${InterviewConstants.Status.Scheduled} and starts from ${interviewStartTimeMoment
803+
.format('MMM DD YYYY HH:mm')} and ends at ${interviewEndTimeMoment
804+
.format('MMM DD YYYY HH:mm')}`
805+
})
806+
} catch (err) {
807+
logger.logFullError(err, { component: 'InterviewService', context: 'partiallyUpdateInterviewByWebhook' })
808+
throw new errors.BadRequestError(`Could not update interview: ${err.message}`)
809+
}
810+
811+
return updatedInterview
812+
}
770813
}
771814

772815
module.exports = {

src/services/NotificationsSchedulerService.js

Lines changed: 13 additions & 24 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,27 +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-
665-
const newUpdate = {
666-
status: entity.status
667-
}
668-
if (entity.startTimestamp) {
669-
newUpdate.startTimestamp = entity.startTimestamp
670-
}
671-
if (entity.endTimestamp) {
672-
newUpdate.endTimestamp = entity.endTimestamp
673-
}
674-
await Interview.update(newUpdate, { where: { id: entity.id } })
675-
await processUpdateInterview(entity)
676-
await helper.postEvent(config.TAAS_INTERVIEW_UPDATE_TOPIC, entity, { oldValue: interviewOldValue.toJSON() })
677-
}
678-
679659
// Send notifications to customer and candidate this interview has expired
680660
async function sendInterviewExpiredNotifications () {
681661
localLogger.debug('[sendInterviewExpiredNotifications]: Looking for due records...')
@@ -715,7 +695,17 @@ async function sendInterviewExpiredNotifications () {
715695
const templateGuest = 'taas.notification.interview-expired-guest'
716696

717697
for (const interview of interviews) {
718-
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+
719709
// send host email
720710
const data = await getDataForInterview(interview)
721711
if (!data) { continue }
@@ -772,7 +762,6 @@ module.exports = {
772762
sendInterviewCompletedNotifications: errorCatchWrapper(sendInterviewCompletedNotifications, 'sendInterviewCompletedNotifications'),
773763
sendInterviewExpiredNotifications: errorCatchWrapper(sendInterviewExpiredNotifications, 'sendInterviewExpiredNotifications'),
774764
sendInterviewScheduleReminderNotifications: errorCatchWrapper(sendInterviewScheduleReminderNotifications, 'sendInterviewScheduleReminderNotifications'),
775-
updateInterviewStatus,
776765
getDataForInterview,
777766
sendPostInterviewActionNotifications: errorCatchWrapper(sendPostInterviewActionNotifications, 'sendPostInterviewActionNotifications'),
778767
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)