Skip to content

Commit b586803

Browse files
authored
Merge pull request #52 from topcoder-platform/interview-scheduler2
Interview scheduler2
2 parents 95800c8 + 7a65b4f commit b586803

File tree

9 files changed

+251
-7
lines changed

9 files changed

+251
-7
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ The following parameters can be set in config files or in env variables:
3535
- `topics.TAAS_WORK_PERIOD_DELETE_TOPIC`: the delete work period entity Kafka message topic
3636
- `topics.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC`: the create work period payment entity Kafka message topic
3737
- `topics.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC`: the update work period payment entity Kafka message topic
38+
- `topics.TAAS_INTERVIEW_REQUEST_TOPIC`: the request interview entity Kafka message topic
39+
- `topics.TAAS_INTERVIEW_UPDATE_TOPIC`: the update interview entity Kafka message topic
40+
- `topics.TAAS_INTERVIEW_BULK_UPDATE_TOPIC`: the bulk update interview entity Kafka message topic
3841
- `esConfig.HOST`: Elasticsearch host
3942
- `esConfig.AWS_REGION`: The Amazon region to use when using AWS Elasticsearch service
4043
- `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

VERIFICATION.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
## Create documents in ES
44

5-
- Run the following commands to create `Job`, `JobCandidate`, `ResourceBooking`, `WorkPeriod`, `WorkPeriodPayment` documents in ES.
5+
- Run the following commands to create `Job`, `JobCandidate`, `Interview`, `ResourceBooking`, `WorkPeriod`, `WorkPeriodPayment` documents in ES.
66

77
``` bash
88
# for Job
99
docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.job.create < test/messages/taas.job.create.event.json
1010
# for JobCandidate
1111
docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.jobcandidate.create < test/messages/taas.jobcandidate.create.event.json
12+
# for Interview
13+
docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.interview.requested < test/messages/taas.interview.requested.event.json
1214
# for ResourceBooking
1315
docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.resourcebooking.create < test/messages/taas.resourcebooking.create.event.json
1416
# for WorkPeriod
@@ -20,13 +22,15 @@
2022
- Run `npm run view-data <model-name-here>` to see if documents were created.
2123

2224
## Update documents in ES
23-
- Run the following commands to update `Job`, `JobCandidate`, `ResourceBooking`, `WorkPeriod`, `WorkPeriodPayment` documents in ES.
25+
- Run the following commands to update `Job`, `JobCandidate`, `Interview`, `ResourceBooking`, `WorkPeriod`, `WorkPeriodPayment` documents in ES.
2426

2527
``` bash
2628
# for Job
2729
docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.job.update < test/messages/taas.job.update.event.json
2830
# for JobCandidate
2931
docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.jobcandidate.update < test/messages/taas.jobcandidate.update.event.json
32+
# for Interview
33+
docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.interview.update < test/messages/taas.interview.update.event.json
3034
# for ResourceBooking
3135
docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.resourcebooking.update < test/messages/taas.resourcebooking.update.event.json
3236
# for WorkPeriod

config/default.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ module.exports = {
3434
TAAS_WORK_PERIOD_DELETE_TOPIC: process.env.TAAS_WORK_PERIOD_DELETE_TOPIC || 'taas.workperiod.delete',
3535
// topics for work period payment service
3636
TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC: process.env.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC || 'taas.workperiodpayment.create',
37-
TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC: process.env.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC || 'taas.workperiodpayment.update'
37+
TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC: process.env.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC || 'taas.workperiodpayment.update',
38+
// topics for interview service
39+
TAAS_INTERVIEW_REQUEST_TOPIC: process.env.TAAS_INTERVIEW_REQUEST_TOPIC || 'taas.interview.requested',
40+
TAAS_INTERVIEW_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_UPDATE_TOPIC || 'taas.interview.update',
41+
TAAS_INTERVIEW_BULK_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_BULK_UPDATE_TOPIC || 'taas.interview.bulkUpdate'
3842
},
3943

4044
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.workperiod.create:1:1,taas.workperiodpayment.create:1:1,taas.job.update:1:1,taas.jobcandidate.update:1:1,taas.resourcebooking.update:1:1,taas.workperiod.update:1:1,taas.workperiodpayment.update:1:1,taas.job.delete:1:1,taas.jobcandidate.delete:1:1,taas.resourcebooking.delete:1:1,taas.workperiod.delete:1:1"
15+
KAFKA_CREATE_TOPICS: "taas.job.create:1:1,taas.jobcandidate.create:1:1,taas.interview.requested:1:1,taas.resourcebooking.create:1:1,taas.workperiod.create:1:1,taas.workperiodpayment.create:1:1,taas.job.update:1:1,taas.jobcandidate.update:1:1,taas.interview.update:1:1,taas.interview.bulkUpdate:1:1,taas.resourcebooking.update:1:1,taas.workperiod.update:1:1,taas.workperiodpayment.update:1:1,taas.job.delete:1:1,taas.jobcandidate.delete:1:1,taas.resourcebooking.delete:1:1,taas.workperiod.delete:1:1"
1616
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
1717
esearch:
1818
image: elasticsearch:7.7.1

src/app.js

Lines changed: 6 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 WorkPeriodPaymentProcessorService = require('./services/WorkPeriodPaymentProcessorService')
1718
const Mutex = require('async-mutex').Mutex
1819
const events = require('events')
@@ -47,7 +48,11 @@ const topicServiceMapping = {
4748
[config.topics.TAAS_WORK_PERIOD_DELETE_TOPIC]: WorkPeriodProcessorService.processDelete,
4849
// work period payment
4950
[config.topics.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC]: WorkPeriodPaymentProcessorService.processCreate,
50-
[config.topics.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC]: WorkPeriodPaymentProcessorService.processUpdate
51+
[config.topics.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC]: WorkPeriodPaymentProcessorService.processUpdate,
52+
// interview
53+
[config.topics.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewProcessorService.processRequestInterview,
54+
[config.topics.TAAS_INTERVIEW_UPDATE_TOPIC]: InterviewProcessorService.processUpdateInterview,
55+
[config.topics.TAAS_INTERVIEW_BULK_UPDATE_TOPIC]: InterviewProcessorService.processBulkUpdateInterviews
5156
}
5257

5358
// Start kafka consumer

src/bootstrap.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
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 allowedXAITemplates = _.values(Interview.XaiTemplate)
8+
const allowedInterviewStatuses = _.values(Interview.Status)
9+
510
global.Promise = require('bluebird')
611

712
Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly')
813
Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled')
9-
Joi.resourceBookingStatus = () => Joi.string().valid('assigned', 'closed', 'cancelled')
10-
Joi.jobCandidateStatus = () => Joi.string().valid('open', 'selected', 'shortlist', 'rejected', 'cancelled', 'interview', 'topcoder-rejected')
14+
Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled')
15+
Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected')
1116
Joi.workload = () => Joi.string().valid('full-time', 'fractional')
1217
Joi.title = () => Joi.string().max(128)
1318
Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled')
19+
Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplates)
20+
Joi.interviewStatus = () => Joi.string().valid(...allowedInterviewStatuses)
1421
Joi.workPeriodPaymentStatus = () => Joi.string().valid('completed', 'cancelled')
1522
// Empty string is not allowed by Joi by default and must be enabled with allow('').
1623
// See https://joi.dev/api/?v=17.3.0#string fro details why it's like this.

src/common/constants.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,19 @@ 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+
},
29+
XaiTemplate: {
30+
'30MinInterview': '30-minutes',
31+
'60MinInterview': '60-minutes'
32+
}
1933
}
2034
}

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+
customMessage: { type: 'text' },
56+
xaiTemplate: { type: 'keyword' },
57+
round: { type: 'integer' },
58+
startTimestamp: { type: 'date' },
59+
attendeesList: [],
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: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Interview Processor Service
3+
*/
4+
5+
const Joi = require('@hapi/joi')
6+
const _ = require('lodash')
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.xaiTemplate().required(),
65+
round: Joi.number().integer().positive().required(),
66+
startTimestamp: Joi.date().allow(null),
67+
attendeesList: Joi.array().items(Joi.string().email()).allow(null),
68+
status: Joi.interviewStatus().required(),
69+
createdAt: Joi.date().required(),
70+
createdBy: Joi.string().uuid().required(),
71+
updatedAt: Joi.date().allow(null),
72+
updatedBy: Joi.string().uuid().allow(null)
73+
}).required()
74+
}).required(),
75+
transactionId: Joi.string().required()
76+
}
77+
78+
/**
79+
* Process update interview entity message
80+
* Updates the 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 interview = message.payload
87+
// if there's an interview with this id,
88+
// update it with the payload
89+
const script = {
90+
source: `
91+
if (ctx._source.containsKey("interviews")) {
92+
def target = ctx._source.interviews.find(i -> i.id == params.interview.id);
93+
if (target != null) {
94+
for (prop in params.interview.entrySet()) {
95+
target[prop.getKey()] = prop.getValue()
96+
}
97+
}
98+
}
99+
`,
100+
params: { interview }
101+
}
102+
await updateJobCandidateViaScript(interview.jobCandidateId, script, transactionId)
103+
}
104+
105+
processUpdateInterview.schema = processRequestInterview.schema
106+
107+
/**
108+
* Process bulk (partially) update interviews entity message.
109+
* Currently supports status, updatedAt and updatedBy fields.
110+
* Update Joi schema to allow more fields.
111+
* (implementation should already handle new fields - just updating Joi schema should be enough)
112+
*
113+
* payload format:
114+
* {
115+
* "jobCandidateId": {
116+
* "interviewId": { ...fields },
117+
* "interviewId2": { ...fields },
118+
* ...
119+
* },
120+
* "jobCandidateId2": { // like above... },
121+
* ...
122+
* }
123+
*
124+
* @param {Object} message the kafka message
125+
* @param {String} transactionId
126+
*/
127+
async function processBulkUpdateInterviews (message, transactionId) {
128+
const jobCandidates = message.payload
129+
// script to update & params
130+
const script = {
131+
source: `
132+
def completedInterviews = params.jobCandidates[ctx._id];
133+
for (interview in completedInterviews.entrySet()) {
134+
def interviewId = interview.getKey();
135+
def affectedFields = interview.getValue();
136+
def target = ctx._source.interviews.find(i -> i.id == interviewId);
137+
if (target != null) {
138+
for (field in affectedFields.entrySet()) {
139+
target[field.getKey()] = field.getValue();
140+
}
141+
}
142+
}
143+
`,
144+
params: { jobCandidates }
145+
}
146+
// update interviews
147+
await esClient.updateByQuery({
148+
index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'),
149+
transactionId,
150+
body: {
151+
script,
152+
query: {
153+
ids: {
154+
values: _.keys(jobCandidates)
155+
}
156+
}
157+
},
158+
refresh: true
159+
})
160+
}
161+
162+
processBulkUpdateInterviews.schema = {
163+
message: Joi.object().keys({
164+
topic: Joi.string().required(),
165+
originator: Joi.string().required(),
166+
timestamp: Joi.date().required(),
167+
'mime-type': Joi.string().required(),
168+
payload: Joi.object().pattern(
169+
Joi.string().uuid(), // key - jobCandidateId
170+
Joi.object().pattern(
171+
Joi.string().uuid(), // inner key - interviewId
172+
Joi.object().keys({
173+
status: Joi.interviewStatus(),
174+
updatedAt: Joi.date(),
175+
updatedBy: Joi.string().uuid()
176+
}) // inner value - affected fields of interview
177+
) // value - object containing interviews
178+
).min(1) // at least one key - i.e. don't allow empty object
179+
}).required(),
180+
transactionId: Joi.string().required()
181+
}
182+
183+
module.exports = {
184+
processRequestInterview,
185+
processUpdateInterview,
186+
processBulkUpdateInterviews
187+
}
188+
189+
logger.buildService(module.exports, 'InterviewProcessorService')

0 commit comments

Comments
 (0)