Skip to content

Commit 36fa12d

Browse files
authored
Merge pull request #43 from xxcxy/feature/interview-scheduler
Interview Scheduler
2 parents f34e1a7 + 47ae415 commit 36fa12d

File tree

8 files changed

+185
-3
lines changed

8 files changed

+185
-3
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ The following parameters can be set in config files or in env variables:
3333
- `topics.TAAS_WORK_PERIOD_CREATE_TOPIC`: the create work period entity Kafka message topic
3434
- `topics.TAAS_WORK_PERIOD_UPDATE_TOPIC`: the update work period entity Kafka message topic
3535
- `topics.TAAS_WORK_PERIOD_DELETE_TOPIC`: the delete work period entity Kafka message topic
36+
- `topics.TAAS_INTERVIEW_REQUEST_TOPIC`: the request interview entity Kafka message topic
37+
- `topics.TAAS_INTERVIEW_UPDATE_TOPIC`: the update interview entity Kafka message topic
3638
- `esConfig.HOST`: Elasticsearch host
3739
- `esConfig.AWS_REGION`: The Amazon region to use when using AWS Elasticsearch service
3840
- `esConfig.ELASTICCLOUD.id`: The elastic cloud id, if your elasticsearch instance is hosted on elastic cloud. DO NOT provide a value for ES_HOST if you are using this

config/default.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ module.exports = {
3131
// topics for work period service
3232
TAAS_WORK_PERIOD_CREATE_TOPIC: process.env.TAAS_WORK_PERIOD_CREATE_TOPIC || 'taas.workperiod.create',
3333
TAAS_WORK_PERIOD_UPDATE_TOPIC: process.env.TAAS_WORK_PERIOD_UPDATE_TOPIC || 'taas.workperiod.update',
34-
TAAS_WORK_PERIOD_DELETE_TOPIC: process.env.TAAS_WORK_PERIOD_DELETE_TOPIC || 'taas.workperiod.delete'
34+
TAAS_WORK_PERIOD_DELETE_TOPIC: process.env.TAAS_WORK_PERIOD_DELETE_TOPIC || 'taas.workperiod.delete',
35+
// topics for interview service
36+
TAAS_INTERVIEW_REQUEST_TOPIC: process.env.TAAS_INTERVIEW_REQUEST_TOPIC || 'taas.interview.requested',
37+
TAAS_INTERVIEW_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_UPDATE_TOPIC || 'taas.interview.update'
3538
},
3639

3740
esConfig: {

local/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ services:
1212
- "9092:9092"
1313
environment:
1414
KAFKA_ADVERTISED_HOST_NAME: localhost
15-
KAFKA_CREATE_TOPICS: "taas.job.create:1:1,taas.jobcandidate.create:1:1,taas.resourcebooking.create:1:1,taas.job.update:1:1,taas.jobcandidate.update:1:1,taas.resourcebooking.update:1:1,taas.job.delete:1:1,taas.jobcandidate.delete:1:1,taas.resourcebooking.delete:1:1"
15+
KAFKA_CREATE_TOPICS: "taas.job.create:1:1,taas.jobcandidate.create:1:1,taas.resourcebooking.create:1:1,taas.interview.requested:1:1,taas.interview.update:1:1,taas.job.update:1:1,taas.jobcandidate.update:1:1,taas.resourcebooking.update:1:1,taas.job.delete:1:1,taas.jobcandidate.delete:1:1,taas.resourcebooking.delete:1:1"
1616
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
1717
esearch:
1818
image: elasticsearch:7.7.1

src/app.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const JobProcessorService = require('./services/JobProcessorService')
1313
const JobCandidateProcessorService = require('./services/JobCandidateProcessorService')
1414
const ResourceBookingProcessorService = require('./services/ResourceBookingProcessorService')
1515
const WorkPeriodProcessorService = require('./services/WorkPeriodProcessorService')
16+
const InterviewProcessorService = require('./services/InterviewProcessorService')
1617
const Mutex = require('async-mutex').Mutex
1718
const events = require('events')
1819

@@ -43,7 +44,10 @@ const topicServiceMapping = {
4344
// work period
4445
[config.topics.TAAS_WORK_PERIOD_CREATE_TOPIC]: WorkPeriodProcessorService.processCreate,
4546
[config.topics.TAAS_WORK_PERIOD_UPDATE_TOPIC]: WorkPeriodProcessorService.processUpdate,
46-
[config.topics.TAAS_WORK_PERIOD_DELETE_TOPIC]: WorkPeriodProcessorService.processDelete
47+
[config.topics.TAAS_WORK_PERIOD_DELETE_TOPIC]: WorkPeriodProcessorService.processDelete,
48+
// interview
49+
[config.topics.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewProcessorService.processRequestInterview,
50+
[config.topics.TAAS_INTERVIEW_UPDATE_TOPIC]: InterviewProcessorService.processUpdateInterview
4751
}
4852

4953
// Start kafka consumer

src/bootstrap.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
const Joi = require('@hapi/joi')
22
const config = require('config')
3+
const _ = require('lodash')
4+
const { Interview } = require('../src/common/constants')
35
const constants = require('./common/constants')
46

7+
const allowedInterviewStatuses = _.values(Interview.Status)
8+
59
global.Promise = require('bluebird')
610

711
Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly')
@@ -10,6 +14,7 @@ Joi.jobCandidateStatus = () => Joi.string().valid('open', 'selected', 'shortlist
1014
Joi.workload = () => Joi.string().valid('full-time', 'fractional')
1115
Joi.title = () => Joi.string().max(128)
1216
Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled')
17+
Joi.interviewStatus = () => Joi.string().valid(...allowedInterviewStatuses)
1318
// Empty string is not allowed by Joi by default and must be enabled with allow('').
1419
// See https://joi.dev/api/?v=17.3.0#string fro details why it's like this.
1520
// In many cases we would like to allow empty string to make it easier to create UI for editing data.

src/common/constants.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,15 @@ module.exports = {
1616
JobCandidateCreate: 'jobcandidate:create',
1717
JobCandidateUpdate: 'jobcandidate:update'
1818
}
19+
},
20+
Interview: {
21+
Status: {
22+
Scheduling: 'Scheduling',
23+
Scheduled: 'Scheduled',
24+
RequestedForReschedule: 'Requested for reschedule',
25+
Rescheduled: 'Rescheduled',
26+
Completed: 'Completed',
27+
Cancelled: 'Cancelled'
28+
}
1929
}
2030
}

src/scripts/createIndex.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ async function createIndex () {
4646
status: { type: 'keyword' },
4747
externalId: { type: 'keyword' },
4848
resume: { type: 'text' },
49+
interviews: {
50+
type: 'nested',
51+
properties: {
52+
id: { type: 'keyword' },
53+
jobCandidateId: { type: 'keyword' },
54+
googleCalendarId: { type: 'keyword' },
55+
startTimestamp: { type: 'date' },
56+
attendeesList: { type: 'keyword' },
57+
customMessage: { type: 'text' },
58+
xaiTemplate: { type: 'keyword' },
59+
round: { type: 'integer' },
60+
status: { type: 'keyword' },
61+
createdAt: { type: 'date' },
62+
createdBy: { type: 'keyword' },
63+
updatedAt: { type: 'date' },
64+
updatedBy: { type: 'keyword' }
65+
}
66+
},
4967
createdAt: { type: 'date' },
5068
createdBy: { type: 'keyword' },
5169
updatedAt: { type: 'date' },
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* Interview Processor Service
3+
*/
4+
5+
const _ = require('lodash')
6+
const Joi = require('@hapi/joi')
7+
const logger = require('../common/logger')
8+
const helper = require('../common/helper')
9+
const constants = require('../common/constants')
10+
const config = require('config')
11+
12+
const esClient = helper.getESClient()
13+
14+
/**
15+
* Updates jobCandidate via a painless script
16+
*
17+
* @param {String} jobCandidateId job candidate id
18+
* @param {String} script script definition
19+
* @param {String} transactionId transaction id
20+
*/
21+
async function updateJobCandidateViaScript (jobCandidateId, script, transactionId) {
22+
await esClient.updateExtra({
23+
index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'),
24+
id: jobCandidateId,
25+
transactionId,
26+
body: { script },
27+
refresh: constants.esRefreshOption
28+
})
29+
}
30+
31+
/**
32+
* Process request interview entity message.
33+
* Creates an interview record under jobCandidate.
34+
*
35+
* @param {Object} message the kafka message
36+
* @param {String} transactionId
37+
*/
38+
async function processRequestInterview (message, transactionId) {
39+
const interview = message.payload
40+
// add interview in collection if there's already an existing collection
41+
// or initiate a new one with this interview
42+
const script = {
43+
source: `
44+
ctx._source.containsKey("interviews")
45+
? ctx._source.interviews.add(params.interview)
46+
: ctx._source.interviews = [params.interview]
47+
`,
48+
params: { interview }
49+
}
50+
await updateJobCandidateViaScript(interview.jobCandidateId, script, transactionId)
51+
}
52+
53+
processRequestInterview.schema = {
54+
message: Joi.object().keys({
55+
topic: Joi.string().required(),
56+
originator: Joi.string().required(),
57+
timestamp: Joi.date().required(),
58+
'mime-type': Joi.string().required(),
59+
payload: Joi.object().keys({
60+
id: Joi.string().uuid().required(),
61+
jobCandidateId: Joi.string().uuid().required(),
62+
googleCalendarId: Joi.string().allow(null),
63+
customMessage: Joi.string().allow(null),
64+
xaiTemplate: Joi.string().required(),
65+
round: Joi.number().integer().positive().required(),
66+
status: Joi.interviewStatus().required(),
67+
createdAt: Joi.date().required(),
68+
createdBy: Joi.string().uuid().required(),
69+
updatedAt: Joi.date().allow(null),
70+
updatedBy: Joi.string().uuid().allow(null),
71+
attendeesList: Joi.array().items(Joi.string().email()).allow(null),
72+
startTimestamp: Joi.date().allow(null)
73+
}).required()
74+
}).required(),
75+
transactionId: Joi.string().required()
76+
}
77+
78+
/**
79+
* Process update interview entity message.
80+
* Update an interview record under jobCandidate.
81+
*
82+
* @param {Object} message the kafka message
83+
* @param {String} transactionId
84+
*/
85+
async function processUpdateInterview (message, transactionId) {
86+
const data = message.payload
87+
const { body: jobCandidate } = await esClient.getExtra({
88+
index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'),
89+
id: data.jobCandidateId
90+
})
91+
const interviews = jobCandidate.interviews || []
92+
const index = _.findIndex(interviews, ['id', data.id])
93+
if (index === -1) {
94+
interviews.push(data)
95+
} else {
96+
interviews.splice(index, 1, data)
97+
}
98+
jobCandidate.interviews = interviews
99+
await esClient.updateExtra({
100+
index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'),
101+
id: data.jobCandidateId,
102+
transactionId,
103+
body: {
104+
doc: jobCandidate
105+
},
106+
refresh: constants.esRefreshOption
107+
})
108+
}
109+
110+
processUpdateInterview.schema = {
111+
message: Joi.object().keys({
112+
topic: Joi.string().required(),
113+
originator: Joi.string().required(),
114+
timestamp: Joi.date().required(),
115+
'mime-type': Joi.string().required(),
116+
payload: Joi.object().keys({
117+
id: Joi.string().uuid().required(),
118+
jobCandidateId: Joi.string().uuid().required(),
119+
googleCalendarId: Joi.string().allow(null),
120+
customMessage: Joi.string().allow(null),
121+
xaiTemplate: Joi.string().required(),
122+
round: Joi.number().integer().positive().required(),
123+
status: Joi.interviewStatus().required(),
124+
createdAt: Joi.date().required(),
125+
createdBy: Joi.string().uuid().required(),
126+
updatedAt: Joi.date().required(),
127+
updatedBy: Joi.string().uuid().required(),
128+
attendeesList: Joi.array().items(Joi.string().email()).allow(null),
129+
startTimestamp: Joi.date().allow(null)
130+
}).required()
131+
}).required(),
132+
transactionId: Joi.string().required()
133+
}
134+
135+
module.exports = {
136+
processRequestInterview,
137+
processUpdateInterview
138+
}
139+
140+
logger.buildService(module.exports, 'InterviewProcessorService')

0 commit comments

Comments
 (0)