diff --git a/README.md b/README.md index a7698cde..d9481f21 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,12 @@ ### Steps to run locally +0. Make sure to use Node v12+ by command `node -v`. We recommend using [NVM](https://github.com/nvm-sh/nvm) to quickly switch to the right version: + + ```bash + nvm use + ``` + 1. 📦 Install npm dependencies ```bash @@ -38,7 +44,7 @@ INTERVIEW_INVITATION_SENDGRID_TEMPLATE_ID= INTERVIEW_INVITATION_SENDER_EMAIL= # Locally deployed services (via docker-compose) - ES_HOST=dockerhost:9200 + ES_HOST=http://dockerhost:9200 DATABASE_URL=postgres://postgres:postgres@dockerhost:5432/postgres BUSAPI_URL=http://dockerhost:8002/v5 ``` @@ -157,6 +163,18 @@ Runs the Topcoder TaaS API using nodemon, so it would be restarted after any of the files is updated. The Topcoder TaaS API will be served on `http://localhost:3000`. +### Working on `taas-es-processor` locally + +When you run `taas-apis` locally as per "[Steps to run locally](#steps-to-run-locally)" the [taas-es-processor](https://github.com/topcoder-platform/taas-es-processor) would be run for you automatically together with other services inside the docker container via `npm run services:up`. + +To be able to change and test `taas-es-processor` locally you can follow the next steps: + +1. Stop `taas-es-processor` inside docker by running `docker-compose -f local/docker-compose.yml stop taas-es-processor` +2. Run `taas-es-processor` separately from the source code. As `npm run services:up` already run all the dependencies for both `taas-apis` and for `taas-es-processor`. The only thing you need to do for running `taas-es-processor` locally is clone the [taas-es-processor](https://github.com/topcoder-platform/taas-es-processor) repository and inside `taas-es-processor` folder run: + - `nvm use` - to use correct Node version + - `npm run install` + - `npm run start` + ## NPM Commands | Command                    | Description | diff --git a/data/demo-data.json b/data/demo-data.json index 5736c839..2c7a94a7 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -436,10 +436,14 @@ { "id": "81f03238-1ce2-4d3d-80c5-5ecd5e7e94a2", "jobCandidateId": "25787cb2-d876-4883-b533-d5e628d213ce", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "30-min-interview", + "calendarEventId": "dummyId", + "templateUrl": "interview-30", "round": 1, + "duration": 30, + "hostName": "John Doe", + "hostEmail": "testuserforemail@yopmail.com", + "guestNames": ["Customer Test"], + "guestEmails": ["testcustomer@yopmail.com"], "status": "Scheduling", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, @@ -449,10 +453,14 @@ { "id": "75363f1d-46c3-4261-9c21-70019f90a61a", "jobCandidateId": "25787cb2-d876-4883-b533-d5e628d213ce", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "30-min-interview", + "calendarEventId": "dummyId", + "templateUrl": "interview-30", "round": 2, + "duration": 30, + "hostName": "John Doe", + "hostEmail": "testuserforemail@yopmail.com", + "guestNames": ["Customer Test"], + "guestEmails": ["testcustomer@yopmail.com"], "status": "Scheduling", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 52000d57..a6afdc2b 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "059739c9-5726-44b6-876d-6d87940c9aff", + "_postman_id": "3f80d93d-6ca3-4645-970d-a9e533394e2e", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -3186,6 +3186,202 @@ }, "response": [] }, + { + "name": "create job candidate", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200)\r", + " if(pm.response.status === \"OK\"){\r", + " const response = pm.response.json()\r", + " pm.environment.set(\"jobCandidateId_2\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobCandidates", + "host": [ + "{{URL}}" + ], + "path": [ + "jobCandidates" + ] + } + }, + "response": [] + }, + { + "name": "create job candidate", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200)\r", + " if(pm.response.status === \"OK\"){\r", + " const response = pm.response.json()\r", + " pm.environment.set(\"jobCandidateId_3\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobCandidates", + "host": [ + "{{URL}}" + ], + "path": [ + "jobCandidates" + ] + } + }, + "response": [] + }, + { + "name": "create job candidate", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200)\r", + " if(pm.response.status === \"OK\"){\r", + " const response = pm.response.json()\r", + " pm.environment.set(\"jobCandidateId_4\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobCandidates", + "host": [ + "{{URL}}" + ], + "path": [ + "jobCandidates" + ] + } + }, + "response": [] + }, + { + "name": "create job candidate", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200)\r", + " if(pm.response.status === \"OK\"){\r", + " const response = pm.response.json()\r", + " pm.environment.set(\"jobCandidateId_5\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobCandidates", + "host": [ + "{{URL}}" + ], + "path": [ + "jobCandidates" + ] + } + }, + "response": [] + }, { "name": "Create completed interview", "event": [ @@ -3217,7 +3413,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\",\r\n \"status\": \"Completed\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\",\r\n \"status\": \"Completed\"\r\n}", "options": { "raw": { "language": "json" @@ -3251,6 +3447,7 @@ " pm.response.to.have.status(200)\r", " if(pm.response.status === \"OK\"){\r", " const response = pm.response.json()\r", + " pm.environment.set(\"interviewId\", response.id);\r", " pm.environment.set(\"interviewRound\", response.round);\r", " }\r", "});" @@ -3270,7 +3467,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\",\r\n \"googleCalendarId\": \"dummyId\",\r\n \"customMessage\": \"This is a custom message\",\r\n \"attendeesList\": [\"attendee1@yopmail.com\", \"attendee2@yopmail.com\"],\r\n \"status\": \"Scheduling\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"calendarEventId\": \"dummyId\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\",\r\n \"guestEmails\": [\"attendee1@yopmail.com\", \"attendee2@yopmail.com\"],\r\n \"status\": \"Scheduling\"\r\n}", "options": { "raw": { "language": "json" @@ -3304,6 +3501,56 @@ " const lastRound = pm.environment.get(\"interviewRound\");\r", " pm.expect(response.round).to.eq(lastRound + 1);\r", " pm.environment.set(\"interviewRound\", response.round);\r", + " pm.environment.set(\"interviewId\", response.id);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_bookingManager}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"calendarEventId\": \"dummyId\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\",\r\n \"status\": \"Scheduling\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}/requestInterview", + "host": [ + "{{URL}}" + ], + "path": [ + "jobCandidates", + "{{jobCandidateId}}", + "requestInterview" + ] + } + }, + "response": [] + }, + { + "name": "Request 4th interview - should not allow", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(409);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You've reached the maximum allowed number (3) of interviews for this candidate.\")\r", "});" ], "type": "text/javascript" @@ -3321,7 +3568,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\",\r\n \"googleCalendarId\": \"dummyId\",\r\n \"customMessage\": \"This is a custom message\",\r\n \"status\": \"Scheduling\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -3373,7 +3620,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -3381,13 +3628,13 @@ } }, "url": { - "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}/requestInterview", + "raw": "{{URL}}/jobCandidates/{{jobCandidateId_2}}/requestInterview", "host": [ "{{URL}}" ], "path": [ "jobCandidates", - "{{jobCandidateId}}", + "{{jobCandidateId_2}}", "requestInterview" ] } @@ -3422,7 +3669,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\",\r\n \"status\": \"xxxx\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"status\": \"xxxx\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -3444,7 +3691,7 @@ "response": [] }, { - "name": "Request interview with attendeesList", + "name": "Request interview with guestEmails", "event": [ { "listen": "test", @@ -3453,8 +3700,8 @@ "pm.test('Status code is 200', function () {\r", " pm.response.to.have.status(200);\r", " const response = pm.response.json()\r", - " pm.expect(response.attendeesList[0]).to.eq(\"attendee1@yopmail.com\")\r", - " pm.expect(response.attendeesList[1]).to.eq(\"attendee2@yopmail.com\")\r", + " pm.expect(response.guestEmails[0]).to.eq(\"attendee1@yopmail.com\")\r", + " pm.expect(response.guestEmails[1]).to.eq(\"attendee2@yopmail.com\")\r", "});" ], "type": "text/javascript" @@ -3472,7 +3719,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\",\r\n \"attendeesList\": [\"attendee1@yopmail.com\", \"attendee2@yopmail.com\"]\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\",\r\n \"guestEmails\": [\"attendee1@yopmail.com\", \"attendee2@yopmail.com\"]\r\n}", "options": { "raw": { "language": "json" @@ -3480,13 +3727,13 @@ } }, "url": { - "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}/requestInterview", + "raw": "{{URL}}/jobCandidates/{{jobCandidateId_2}}/requestInterview", "host": [ "{{URL}}" ], "path": [ "jobCandidates", - "{{jobCandidateId}}", + "{{jobCandidateId_2}}", "requestInterview" ] } @@ -3494,7 +3741,7 @@ "response": [] }, { - "name": "Request interview with invalid attendeesList", + "name": "Request interview with invalid guestEmails", "event": [ { "listen": "test", @@ -3503,7 +3750,7 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"\\\"interview.attendeesList\\\" must be an array\")\r", + " pm.expect(response.message).to.eq(\"\\\"interview.guestEmails\\\" must be an array\")\r", "});" ], "type": "text/javascript" @@ -3521,7 +3768,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\",\r\n \"attendeesList\": \"asddd\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\",\r\n \"guestEmails\": \"asddd\"\r\n}", "options": { "raw": { "language": "json" @@ -3543,7 +3790,7 @@ "response": [] }, { - "name": "Request interview with invalid attendee email", + "name": "Request interview with invalid guest email", "event": [ { "listen": "test", @@ -3552,7 +3799,7 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"\\\"interview.attendeesList[0]\\\" must be a valid email\")\r", + " pm.expect(response.message).to.eq(\"\\\"interview.guestEmails[0]\\\" must be a valid email\")\r", "});" ], "type": "text/javascript" @@ -3570,7 +3817,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\",\r\n \"attendeesList\": [\"asdas\"]\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\",\r\n \"guestEmails\": [\"asdas\"]\r\n}", "options": { "raw": { "language": "json" @@ -3619,7 +3866,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\",\r\n \"round\": 1\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\",\r\n \"round\": 1\r\n}", "options": { "raw": { "language": "json" @@ -3668,7 +3915,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\",\r\n \"startTimestamp\": \"2021-04-17\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\",\r\n \"startTimestamp\": \"2021-04-17\"\r\n}", "options": { "raw": { "language": "json" @@ -3690,7 +3937,7 @@ "response": [] }, { - "name": "Request interview without xaiTemplate - should fail", + "name": "Request interview without templateUrl - should fail", "event": [ { "listen": "test", @@ -3699,7 +3946,7 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"\\\"interview.xaiTemplate\\\" is required\")\r", + " pm.expect(response.message).to.eq(\"\\\"interview.templateUrl\\\" is required\")\r", "});" ], "type": "text/javascript" @@ -3717,7 +3964,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"status\": \"Requested\"\r\n}", + "raw": "{\r\n \"status\": \"Scheduling\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -3739,7 +3986,7 @@ "response": [] }, { - "name": "Request interview with invalid xaiTemplate", + "name": "Request interview with invalid templateUrl", "event": [ { "listen": "test", @@ -3748,7 +3995,7 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"\\\"interview.xaiTemplate\\\" must be one of [interview-30, interview-60]\")\r", + " pm.expect(response.message).to.eq(\"\\\"interview.templateUrl\\\" must be one of [interview-30, interview-60]\")\r", "});" ], "type": "text/javascript" @@ -3766,7 +4013,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"asdas\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"asdas\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -3815,7 +4062,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -3864,7 +4111,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -3872,13 +4119,13 @@ } }, "url": { - "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}/requestInterview", + "raw": "{{URL}}/jobCandidates/{{jobCandidateId_2}}/requestInterview", "host": [ "{{URL}}" ], "path": [ "jobCandidates", - "{{jobCandidateId}}", + "{{jobCandidateId_2}}", "requestInterview" ] } @@ -3911,7 +4158,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -3919,13 +4166,13 @@ } }, "url": { - "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}/requestInterview", + "raw": "{{URL}}/jobCandidates/{{jobCandidateId_3}}/requestInterview", "host": [ "{{URL}}" ], "path": [ "jobCandidates", - "{{jobCandidateId}}", + "{{jobCandidateId_3}}", "requestInterview" ] } @@ -3958,7 +4205,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -3966,13 +4213,13 @@ } }, "url": { - "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}/requestInterview", + "raw": "{{URL}}/jobCandidates/{{jobCandidateId_3}}/requestInterview", "host": [ "{{URL}}" ], "path": [ "jobCandidates", - "{{jobCandidateId}}", + "{{jobCandidateId_3}}", "requestInterview" ] } @@ -4005,7 +4252,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -4013,13 +4260,13 @@ } }, "url": { - "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}/requestInterview", + "raw": "{{URL}}/jobCandidates/{{jobCandidateId_3}}/requestInterview", "host": [ "{{URL}}" ], "path": [ "jobCandidates", - "{{jobCandidateId}}", + "{{jobCandidateId_3}}", "requestInterview" ] } @@ -4052,7 +4299,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -4060,13 +4307,13 @@ } }, "url": { - "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}/requestInterview", + "raw": "{{URL}}/jobCandidates/{{jobCandidateId_4}}/requestInterview", "host": [ "{{URL}}" ], "path": [ "jobCandidates", - "{{jobCandidateId}}", + "{{jobCandidateId_4}}", "requestInterview" ] } @@ -4101,7 +4348,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -4150,7 +4397,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -4199,7 +4446,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -4207,13 +4454,13 @@ } }, "url": { - "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}/requestInterview", + "raw": "{{URL}}/jobCandidates/{{jobCandidateId_4}}/requestInterview", "host": [ "{{URL}}" ], "path": [ "jobCandidates", - "{{jobCandidateId}}", + "{{jobCandidateId_4}}", "requestInterview" ] } @@ -4248,7 +4495,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -4256,13 +4503,13 @@ } }, "url": { - "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}/requestInterview", + "raw": "{{URL}}/jobCandidates/{{jobCandidateId_4}}/requestInterview", "host": [ "{{URL}}" ], "path": [ "jobCandidates", - "{{jobCandidateId}}", + "{{jobCandidateId_4}}", "requestInterview" ] } @@ -4297,7 +4544,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -4305,13 +4552,13 @@ } }, "url": { - "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}/requestInterview", + "raw": "{{URL}}/jobCandidates/{{jobCandidateId_5}}/requestInterview", "host": [ "{{URL}}" ], "path": [ "jobCandidates", - "{{jobCandidateId}}", + "{{jobCandidateId_5}}", "requestInterview" ] } @@ -4340,7 +4587,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -4389,7 +4636,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\"\r\n}", "options": { "raw": { "language": "json" @@ -4410,6 +4657,86 @@ }, "response": [] }, + { + "name": "Get interview by id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200)\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_bookingManager}}", + "type": "text" + } + ], + "url": { + "raw": "{{URL}}/getInterview/{{interviewId}}", + "host": [ + "{{URL}}" + ], + "path": [ + "getInterview", + "{{interviewId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get interview by id fromDb", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200)\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_bookingManager}}", + "type": "text" + } + ], + "url": { + "raw": "{{URL}}/getInterview/{{interviewId}}?fromDb=true", + "host": [ + "{{URL}}" + ], + "path": [ + "getInterview", + "{{interviewId}}" + ], + "query": [ + { + "key": "fromDb", + "value": "true" + } + ] + } + }, + "response": [] + }, { "name": "Get interview by round", "event": [ @@ -5080,7 +5407,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"googleCalendarId\": \"dummyIdXX\",\r\n \"customMessage\": \"This is the updated message\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"dummyIdXX\",\r\n \"xaiId\": \"0edc1751-f4ca-4e8e-908a-95f6560311ab\"\r\n}", "options": { "raw": { "language": "json" @@ -5088,13 +5415,13 @@ } }, "url": { - "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}/updateInterview/{{interviewRound}}", + "raw": "{{URL}}/jobCandidates/{{jobCandidateId_4}}/updateInterview/{{interviewRound}}", "host": [ "{{URL}}" ], "path": [ "jobCandidates", - "{{jobCandidateId}}", + "{{jobCandidateId_4}}", "updateInterview", "{{interviewRound}}" ] @@ -5303,7 +5630,7 @@ "response": [] }, { - "name": "Update interview with invalid xaiTemplate", + "name": "Update interview with invalid templateUrl", "event": [ { "listen": "test", @@ -5312,7 +5639,7 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"\\\"data.xaiTemplate\\\" must be one of [interview-30, interview-60]\")\r", + " pm.expect(response.message).to.eq(\"\\\"data.templateUrl\\\" must be one of [interview-30, interview-60]\")\r", "});" ], "type": "text/javascript" @@ -5330,7 +5657,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"xxx\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"xxx\"\r\n}", "options": { "raw": { "language": "json" @@ -5353,7 +5680,7 @@ "response": [] }, { - "name": "Update interview status to Scheduled without googleCalendarId", + "name": "Update interview status to Scheduled without calendarEventId", "event": [ { "listen": "test", @@ -5362,7 +5689,7 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"\\\"data.googleCalendarId\\\" is required\")\r", + " pm.expect(response.message).to.eq(\"\\\"data.calendarEventId\\\" is required\")\r", "});" ], "type": "text/javascript" @@ -5430,7 +5757,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"googleCalendarId\": \"sdsds\",\r\n \"status\": \"Scheduled\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"sdsds\",\r\n \"status\": \"Scheduled\"\r\n}", "options": { "raw": { "language": "json" @@ -5453,7 +5780,7 @@ "response": [] }, { - "name": "Update interview status to Rescheduled without googleCalendarId", + "name": "Update interview status to Rescheduled without calendarEventId", "event": [ { "listen": "test", @@ -5462,7 +5789,7 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"\\\"data.googleCalendarId\\\" is required\")\r", + " pm.expect(response.message).to.eq(\"\\\"data.calendarEventId\\\" is required\")\r", "});" ], "type": "text/javascript" @@ -5530,7 +5857,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"googleCalendarId\": \"sdsds\",\r\n \"status\": \"Rescheduled\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"sdsds\",\r\n \"status\": \"Rescheduled\"\r\n}", "options": { "raw": { "language": "json" @@ -5580,7 +5907,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"googleCalendarId\": \"sdsds\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"sdsds\"\r\n}", "options": { "raw": { "language": "json" @@ -5630,7 +5957,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"googleCalendarId\": \"sdsds\",\r\n \"status\": \"Scheduling\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"sdsds\",\r\n \"status\": \"Scheduling\"\r\n}", "options": { "raw": { "language": "json" @@ -5728,7 +6055,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"customMessage\": \"sdsds\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"sdsds\"\r\n}", "options": { "raw": { "language": "json" @@ -5776,7 +6103,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"customMessage\": \"sdsds\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"sdsds\"\r\n}", "options": { "raw": { "language": "json" @@ -5824,7 +6151,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"customMessage\": \"sdsds\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"sdsds\"\r\n}", "options": { "raw": { "language": "json" @@ -5872,7 +6199,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"customMessage\": \"sdsds\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"sdsds\"\r\n}", "options": { "raw": { "language": "json" @@ -5922,7 +6249,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"customMessage\": \"sdsds\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"sdsds\"\r\n}", "options": { "raw": { "language": "json" @@ -5972,7 +6299,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"customMessage\": \"sdsds\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"sdsds\"\r\n}", "options": { "raw": { "language": "json" @@ -6022,7 +6349,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"customMessage\": \"sdsds\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"sdsds\"\r\n}", "options": { "raw": { "language": "json" @@ -6072,7 +6399,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"customMessage\": \"sdsds\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"sdsds\"\r\n}", "options": { "raw": { "language": "json" @@ -6122,7 +6449,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"customMessage\": \"sdsds\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"sdsds\"\r\n}", "options": { "raw": { "language": "json" @@ -6210,13 +6537,187 @@ "header": [ { "key": "Authorization", - "value": "Bearer invalid_key", + "value": "Bearer invalid_key", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"customMessage\": \"sdsds\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}/updateInterview/{{interviewRound}}", + "host": [ + "{{URL}}" + ], + "path": [ + "jobCandidates", + "{{jobCandidateId}}", + "updateInterview", + "{{interviewRound}}" + ] + } + }, + "response": [] + }, + { + "name": "Get interview by xaiId", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200)\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_bookingManager}}", + "type": "text" + } + ], + "url": { + "raw": "{{URL}}/getInterview/0edc1751-f4ca-4e8e-908a-95f6560311ab", + "host": [ + "{{URL}}" + ], + "path": [ + "getInterview", + "0edc1751-f4ca-4e8e-908a-95f6560311ab" + ] + } + }, + "response": [] + }, + { + "name": "Get interview by xaiId fromDb", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200)\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_bookingManager}}", + "type": "text" + } + ], + "url": { + "raw": "{{URL}}/getInterview/0edc1751-f4ca-4e8e-908a-95f6560311ab?fromDb=true", + "host": [ + "{{URL}}" + ], + "path": [ + "getInterview", + "0edc1751-f4ca-4e8e-908a-95f6560311ab" + ], + "query": [ + { + "key": "fromDb", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "Update interview by id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200)\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_bookingManager}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"xaiId\": \"0edc1751-f4ca-4e8e-908a-95f6560311ab\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/updateInterview/{{interviewId}}", + "host": [ + "{{URL}}" + ], + "path": [ + "updateInterview", + "{{interviewId}}" + ] + } + }, + "response": [] + }, + { + "name": "Update interview by xaiId", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200)\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_bookingManager}}", "type": "text" } ], "body": { "mode": "raw", - "raw": "{\r\n \"customMessage\": \"sdsds\"\r\n}", + "raw": "{\r\n \"xaiId\": \"0edc1751-f4ca-4e8e-908a-95f6560311ab\",\r\n \"calendarEventId\": \"test\"\r\n}", "options": { "raw": { "language": "json" @@ -6224,15 +6725,13 @@ } }, "url": { - "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}/updateInterview/{{interviewRound}}", + "raw": "{{URL}}/updateInterview/0edc1751-f4ca-4e8e-908a-95f6560311ab", "host": [ "{{URL}}" ], "path": [ - "jobCandidates", - "{{jobCandidateId}}", "updateInterview", - "{{interviewRound}}" + "0edc1751-f4ca-4e8e-908a-95f6560311ab" ] } }, @@ -19431,6 +19930,7 @@ " if(pm.response.status === \"OK\"){\r", " const response = pm.response.json()\r", " pm.environment.set(\"interview_round_created_by_administrator\", response.round);\r", + " pm.environment.set(\"interview_id_created_by_administrator\", response.id);\r", " }\r", "});" ], @@ -19449,7 +19949,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\",\r\n \"googleCalendarId\": \"dummyId\",\r\n \"customMessage\": \"This is a custom message\",\r\n \"status\": \"Scheduling\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\",\r\n \"calendarEventId\": \"dummyId\",\r\n \"status\": \"Scheduling\"\r\n}", "options": { "raw": { "language": "json" @@ -19471,7 +19971,7 @@ "response": [] }, { - "name": "✔ get interview with administrator", + "name": "✔ get interview by round with administrator", "event": [ { "listen": "test", @@ -19510,7 +20010,44 @@ "response": [] }, { - "name": "✔ update interview with administrator", + "name": "✔ get interview by id with administrator", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "url": { + "raw": "{{URL}}/getInterview/{{interview_id_created_by_administrator}}", + "host": [ + "{{URL}}" + ], + "path": [ + "getInterview", + "{{interview_id_created_by_administrator}}" + ] + } + }, + "response": [] + }, + { + "name": "✔ update interview by round with administrator", "event": [ { "listen": "test", @@ -19535,7 +20072,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"customMessage\": \"updated\"\r\n}", + "raw": "{\r\n \"calendarEventId\": \"updated\"\r\n}", "options": { "raw": { "language": "json" @@ -19557,6 +20094,52 @@ }, "response": [] }, + { + "name": "✔ update interview by id with administrator", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"xaiId\": \"0edc1751-f4ca-4e8e-908a-95f6560311ab\",\r\n \"calendarEventId\": \"updated\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/updateInterview/{{interview_id_created_by_administrator}}", + "host": [ + "{{URL}}" + ], + "path": [ + "updateInterview", + "{{interview_id_created_by_administrator}}" + ] + } + }, + "response": [] + }, { "name": "✔ search interviews with administrator", "event": [ @@ -21372,6 +21955,7 @@ " pm.response.to.have.status(200);\r", " const response = pm.response.json()\r", " pm.environment.set(\"interview_round_created_for_member\", response.round)\r", + " pm.environment.set(\"interview_id_created_for_member\", response.id)\r", "});" ], "type": "text/javascript" @@ -21389,7 +21973,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\",\r\n \"googleCalendarId\": \"dummyId\",\r\n \"customMessage\": \"This is a custom message\",\r\n \"status\": \"Scheduling\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"calendarEventId\": \"dummyId\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\",\r\n \"status\": \"Scheduling\"\r\n}", "options": { "raw": { "language": "json" @@ -21419,10 +22003,10 @@ "listen": "test", "script": { "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + " pm.expect(response.message).to.eq(\"userId: 8547899 the user is not a member of project 111\")\r", "});" ], "type": "text/javascript" @@ -21440,7 +22024,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\",\r\n \"googleCalendarId\": \"dummyId\",\r\n \"customMessage\": \"This is a custom message\",\r\n \"status\": \"Scheduling\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\",\r\n \"calendarEventId\": \"dummyId\",\r\n \"status\": \"Scheduling\"\r\n}", "options": { "raw": { "language": "json" @@ -21462,16 +22046,16 @@ "response": [] }, { - "name": "✘ get interview with member", + "name": "✘ get interview by round with member", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + " pm.expect(response.message).to.eq(\"userId: 8547899 the user is not a member of project 111\")\r", "});" ], "type": "text/javascript" @@ -21503,16 +22087,55 @@ "response": [] }, { - "name": "✘ update interview with member", + "name": "✘ get interview by id with member", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + " pm.expect(response.message).to.eq(\"userId: 8547899 the user is not a member of project 111\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_member}}", + "type": "text" + } + ], + "url": { + "raw": "{{URL}}/getInterview/{{interview_id_created_for_member}}", + "host": [ + "{{URL}}" + ], + "path": [ + "getInterview", + "{{interview_id_created_for_member}}" + ] + } + }, + "response": [] + }, + { + "name": "✘ update interview by round with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"userId: 8547899 the user is not a member of project 111\")\r", "});" ], "type": "text/javascript" @@ -21530,7 +22153,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"customMessage\": \"updated\"\r\n}", + "raw": "{\r\n \"xaiId\": \"0edc1751-f4ca-4e8e-908a-95f6560311ab\"\r\n}", "options": { "raw": { "language": "json" @@ -21552,6 +22175,54 @@ }, "response": [] }, + { + "name": "✘ update interview by id with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"userId: 8547899 the user is not a member of project 111\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_member}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"xaiId\": \"0edc1751-f4ca-4e8e-908a-95f6560311ab\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/updateInterview/{{interview_id_created_for_member}}", + "host": [ + "{{URL}}" + ], + "path": [ + "updateInterview", + "{{interview_id_created_for_member}}" + ] + } + }, + "response": [] + }, { "name": "✘ search interviews with member", "event": [ @@ -21559,10 +22230,10 @@ "listen": "test", "script": { "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + " pm.expect(response.message).to.eq(\"userId: 8547899 the user is not a member of project 111\")\r", "});" ], "type": "text/javascript" @@ -23526,7 +24197,8 @@ "pm.test('Status code is 200', function () {\r", " pm.response.to.have.status(200);\r", " const response = pm.response.json()\r", - " pm.environment.set(\"interview_round_created_for_connect_manager\", response.round)\r", + " pm.environment.set(\"interview_round_created_for_connect_manager\", response.round);\r", + " pm.environment.set(\"interview_id_created_for_connect_manager\", response.id);\r", "});" ], "type": "text/javascript" @@ -23544,7 +24216,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\",\r\n \"googleCalendarId\": \"dummyId\",\r\n \"customMessage\": \"This is a custom message\",\r\n \"status\": \"Scheduling\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"calendarEventId\": \"dummyId\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\",\r\n \"status\": \"Scheduling\"\r\n}", "options": { "raw": { "language": "json" @@ -23574,10 +24246,10 @@ "listen": "test", "script": { "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + " pm.expect(response.message).to.eq(\"userId: 88774489 the user is not a member of project 111\")\r", "});" ], "type": "text/javascript" @@ -23595,7 +24267,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"xaiTemplate\": \"interview-30\",\r\n \"googleCalendarId\": \"dummyId\",\r\n \"customMessage\": \"This is a custom message\",\r\n \"status\": \"Scheduling\"\r\n}", + "raw": "{\r\n \"templateUrl\": \"interview-30\",\r\n \"calendarEventId\": \"dummyId\",\r\n \"hostEmail\": \"testcustomer@yopmail.com\",\r\n \"status\": \"Scheduling\"\r\n}", "options": { "raw": { "language": "json" @@ -23617,16 +24289,16 @@ "response": [] }, { - "name": "✘ get interview with connect manager", + "name": "✘ get interview by round with connect manager", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + " pm.expect(response.message).to.eq(\"userId: 88774489 the user is not a member of project 111\")\r", "});" ], "type": "text/javascript" @@ -23658,16 +24330,55 @@ "response": [] }, { - "name": "✘ update interview with connect manager", + "name": "✘ get interview by id with connect manager", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + " pm.expect(response.message).to.eq(\"userId: 88774489 the user is not a member of project 111\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_connect_manager_pshahcopmanag2}}", + "type": "text" + } + ], + "url": { + "raw": "{{URL}}/getInterview/{{interview_id_created_for_connect_manager}}", + "host": [ + "{{URL}}" + ], + "path": [ + "getInterview", + "{{interview_id_created_for_connect_manager}}" + ] + } + }, + "response": [] + }, + { + "name": "✘ update interview by round with connect manager", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"userId: 88774489 the user is not a member of project 111\")\r", "});" ], "type": "text/javascript" @@ -23685,7 +24396,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"customMessage\": \"updated\"\r\n}", + "raw": "{\r\n \"xaiId\": \"0edc1751-f4ca-4e8e-908a-95f6560311ab\"\r\n}", "options": { "raw": { "language": "json" @@ -23707,6 +24418,54 @@ }, "response": [] }, + { + "name": "✘ update interview by id with connect manager", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"userId: 88774489 the user is not a member of project 111\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_connect_manager_pshahcopmanag2}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"xaiId\": \"0edc1751-f4ca-4e8e-908a-95f6560311ab\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/updateInterview/{{interview_id_created_for_connect_manager}}", + "host": [ + "{{URL}}" + ], + "path": [ + "updateInterview", + "{{interview_id_created_for_connect_manager}}" + ] + } + }, + "response": [] + }, { "name": "✘ search interviews with member", "event": [ @@ -23714,10 +24473,10 @@ "listen": "test", "script": { "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + " pm.expect(response.message).to.eq(\"userId: 88774489 the user is not a member of project 111\")\r", "});" ], "type": "text/javascript" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 00dab590..0a9fe80d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -5,6 +5,7 @@ info: version: 1.0.0 servers: - url: /api/{apiVersion}/ + - url: http://localhost:3000/v5/ - url: https://api.topcoder-dev.com/{apiVersion}/ variables: apiVersion: @@ -1029,6 +1030,65 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /updateInterview/{id}: + patch: + tags: + - Interviews + description: | + Partially update interview. + + **Authorization** Topcoder token with update interview scope is allowed + security: + - bearerAuth: [] + parameters: + - in: path + name: id + description: The interview or xai id. + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateInterviewRequestBody" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Interview" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /jobCandidates/{jobCandidateId}/interviews: get: tags: @@ -1203,6 +1263,66 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /getInterview/{id}: + get: + tags: + - Interviews + description: | + Get interview by id. + + **Authorization** Topcoder token with read interview scope is allowed + security: + - bearerAuth: [] + parameters: + - in: path + name: id + description: The interview or xai id. + required: true + schema: + type: string + - in: query + name: fromDb + description: get data from db or not. + required: false + schema: + type: boolean + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Interview" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /resourceBookings: post: tags: @@ -3436,7 +3556,7 @@ components: required: - id - jobCandidateId - - xaiTemplate + - templateUrl - round - status - createdAt @@ -3446,41 +3566,78 @@ components: type: string format: uuid description: "The interview id." + xaiId: + type: string + description: "The x.ai id." jobCandidateId: type: string format: uuid description: "The job candidate id." - googleCalendarId: + calendarEventId: type: string example: "dummyId" - description: "The google calendar id." - xaiTemplate: + description: "The calendar event id." + templateUrl: type: string example: "interview-30" enum: ["interview-30", "interview-60"] description: "The x.ai template name" - customMessage: + templateId: + type: string + format: uuid + description: "The x.ai template id" + templateType: + type: string + description: "The x.ai template type" + title: type: string - example: "This is a custom message" - description: "The custom message." + description: "The x.ai template title" + locationDetails: + type: string + example: "Location TBD." + description: "The x.ai meeting location." round: type: integer example: 1 description: "The interview round." - attendeesList: + duration: + type: integer + example: 30 + description: "The interview duration (in minutes)." + hostEmail: + type: string + format: email + description: "The interview host email." + hostName: + type: string + description: "The interview host name." + guestEmails: type: array description: "Attendee list for this interview." items: type: string format: email + guestNames: + type: array + description: "Names of guests." + items: + type: string startTimestamp: type: string format: date-time description: "Interview start time." + endTimestamp: + type: string + format: date-time + description: "Interview end time." status: type: string enum: ["Scheduling", "Scheduled", "Requested for reschedule", "Rescheduled", "Completed", "Cancelled"] description: "The interview status." + rescheduleUrl: + type: string + format: url + description: "x.ai reschedule url." createdAt: type: string format: date-time @@ -3499,22 +3656,22 @@ components: description: "The user who updated the interview last time.(Will get the user info from the token)" RequestInterviewBody: required: - - xaiTemplate + - templateUrl + - hostEmail properties: - googleCalendarId: + calendarEventId: type: string example: "dummyId" - description: "The google calendar id." - customMessage: - type: string - example: "This is a custom message" - description: "The custom message." - xaiTemplate: + description: "The calendar event id." + templateUrl: type: string enum: ["interview-30", "interview-60"] example: "interview-30" description: "The x.ai template name" - attendeesList: + hostEmail: + type: string + format: email + guestEmails: type: array items: type: string @@ -3526,31 +3683,66 @@ components: description: "The interview status." UpdateInterviewRequestBody: properties: - googleCalendarId: + xaiId: + type: string + description: "The x.ai meeting id" + calendarEventId: type: string example: "dummyId" description: "The google calendar id." - customMessage: - type: string - example: "This is a custom message" - description: "The custom message." - xaiTemplate: + templateUrl: type: string enum: ["interview-30", "interview-60"] example: "interview-30" description: "The x.ai template name" - attendeesList: + templateId: + type: string + format: uuid + description: "The x.ai template id" + templateType: + type: string + description: "The x.ai template type" + title: + type: string + description: "The x.ai meeting title" + locationDetails: + type: string + example: "Location TBD." + description: "The x.ai location details" + hostName: + type: string + description: "The host name" + hostEmail: + type: string + format: email + description: "The host email" + guestNames: + type: array + items: + type: string + description: "The guest names" + guestEmails: type: array items: type: string format: email + description: "The guest emails" startTimestamp: type: string format: date-time + description: "Interview start time." + endTimestamp: + type: string + format: date-time + description: "Interview end time." status: type: string enum: ["Scheduling", "Scheduled", "Requested for reschedule", "Rescheduled", "Completed", "Cancelled"] description: "The interview status." + rescheduleUrl: + type: string + format: url + description: "The x.ai reschedule url" JobPatchRequestBody: properties: status: diff --git a/docs/taas-ER-diagram.png b/docs/taas-ER-diagram.png new file mode 100644 index 00000000..9733bbd6 Binary files /dev/null and b/docs/taas-ER-diagram.png differ diff --git a/docs/topcoder-bookings.postman_environment.json b/docs/topcoder-bookings.postman_environment.json index 58304b6d..837b55db 100644 --- a/docs/topcoder-bookings.postman_environment.json +++ b/docs/topcoder-bookings.postman_environment.json @@ -1,5 +1,5 @@ { - "id": "cf10e077-1975-444a-b2d2-3e8e71080526", + "id": "228f4dcc-6914-462e-9b56-3285b643a2f8", "name": "topcoder-bookings", "values": [ { @@ -421,9 +421,49 @@ "key": "token_m2m_read_jobCandidates_all_interviews", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJyZWFkOnRhYXMtam9iQ2FuZGlkYXRlcyBhbGw6dGFhcy1pbnRlcnZpZXdzIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.XQj74JSHp98XKxa1eZnMMpHxGpHeZAHVhLvFAN7gHBY", "enabled": true + }, + { + "key": "jobCandidateId_2", + "value": "", + "enabled": true + }, + { + "key": "jobCandidateId_3", + "value": "", + "enabled": true + }, + { + "key": "jobCandidateId_4", + "value": "", + "enabled": true + }, + { + "key": "jobCandidateId_5", + "value": "", + "enabled": true + }, + { + "key": "interviewId", + "value": "", + "enabled": true + }, + { + "key": "interview_id_created_by_administrator", + "value": "", + "enabled": true + }, + { + "key": "interview_id_created_for_member", + "value": "", + "enabled": true + }, + { + "key": "interview_id_created_for_connect_manager", + "value": "", + "enabled": true } ], "_postman_variable_scope": "environment", - "_postman_exported_at": "2021-04-22T11:06:21.725Z", - "_postman_exported_using": "Postman/8.2.3" + "_postman_exported_at": "2021-05-10T05:06:38.661Z", + "_postman_exported_using": "Postman/8.3.1" } \ No newline at end of file diff --git a/migrations/2021-05-09-interviews-table-migration.js b/migrations/2021-05-09-interviews-table-migration.js new file mode 100644 index 00000000..46730dc1 --- /dev/null +++ b/migrations/2021-05-09-interviews-table-migration.js @@ -0,0 +1,63 @@ +'use strict'; +const config = require('config') + +/** + * Add `xaiId` column. + * Add `rescheduleUrl` column. + * Add `endTimestamp` column. + * Add `duration` column. + * Add `templateId` column. + * Add `templateType` column. + * Add `title` column. + * Add `locationDetails` column. + * Add `hostName` column. + * Add `hostEmail` column. + * Add `guestNames` column. + * Rename column `attendeesList` to `guestEmails` + * Rename column `googleCalendarId` to `calendarEventId` + * Rename column `xaiTemplate` to `templateUrl` + * Remove `customMessage` column + */ +module.exports = { + up: async (queryInterface, Sequelize) => { + const table = { tableName: 'interviews', schema: config.DB_SCHEMA_NAME } + await Promise.all([ + queryInterface.addColumn(table, 'xai_id', { type: Sequelize.STRING(255) }), + queryInterface.addColumn(table, 'duration', { type: Sequelize.INTEGER }), + queryInterface.addColumn(table, 'end_timestamp', { type: Sequelize.DATE }), + queryInterface.addColumn(table, 'template_id', { type: Sequelize.STRING(255) }), + queryInterface.addColumn(table, 'template_type', { type: Sequelize.STRING(255) }), + queryInterface.addColumn(table, 'location_details', { type: Sequelize.STRING(255) }), + queryInterface.addColumn(table, 'title', { type: Sequelize.STRING(255) }), + queryInterface.addColumn(table, 'host_name', { type: Sequelize.STRING(255) }), + queryInterface.addColumn(table, 'host_email', { type: Sequelize.STRING(255) }), + queryInterface.addColumn(table, 'guest_names', { type: Sequelize.ARRAY(Sequelize.STRING) }), + queryInterface.addColumn(table, 'reschedule_url', { type: Sequelize.STRING(255) }), + queryInterface.renameColumn(table, 'attendees_list', 'guest_emails'), + queryInterface.renameColumn(table, 'google_calendar_id', 'calendar_event_id'), + queryInterface.renameColumn(table, 'xai_template', 'template_url'), + queryInterface.removeColumn(table, 'custom_message') + ]) + }, + + down: async (queryInterface, Sequelize) => { + const table = { tableName: 'interviews', schema: config.DB_SCHEMA_NAME } + await Promise.all([ + queryInterface.removeColumn(table, 'xai_id'), + queryInterface.removeColumn(table, 'duration'), + queryInterface.removeColumn(table, 'end_timestamp'), + queryInterface.removeColumn(table, 'template_id'), + queryInterface.removeColumn(table, 'template_type'), + queryInterface.removeColumn(table, 'location_details'), + queryInterface.removeColumn(table, 'title'), + queryInterface.removeColumn(table, 'host_name'), + queryInterface.removeColumn(table, 'host_email'), + queryInterface.removeColumn(table, 'guest_names'), + queryInterface.removeColumn(table, 'reschedule_url'), + queryInterface.renameColumn(table, 'guest_emails', 'attendees_list'), + queryInterface.renameColumn(table, 'calendar_event_id', 'google_calendar_id'), + queryInterface.renameColumn(table, 'template_url', 'xai_template'), + queryInterface.addColumn(table, 'custom_message', { type: Sequelize.TEXT }) + ]) + } +}; diff --git a/package-lock.json b/package-lock.json index 76b17135..3d174e12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -498,9 +498,9 @@ }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "requires": { "ms": "^2.1.1" } @@ -526,9 +526,9 @@ } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "readable-stream": { "version": "2.3.7", @@ -600,20 +600,20 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, "@types/connect": { - "version": "3.4.33", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", - "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", "requires": { "@types/node": "*" } }, "@types/express": { - "version": "4.17.8", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.8.tgz", - "integrity": "sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ==", + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.11.tgz", + "integrity": "sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg==", "requires": { "@types/body-parser": "*", - "@types/express-serve-static-core": "*", + "@types/express-serve-static-core": "^4.17.18", "@types/qs": "*", "@types/serve-static": "*" } @@ -628,9 +628,9 @@ } }, "@types/express-serve-static-core": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz", - "integrity": "sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz", + "integrity": "sha512-DJOSHzX7pCiSElWaGR8kCprwibCB/3yW6vcT8VG3P0SJjnv19gnWG/AZMfM60Xj/YJIp/YCaDHyvzsFVeniARA==", "requires": { "@types/node": "*", "@types/qs": "*", @@ -646,9 +646,9 @@ } }, "@types/mime": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", - "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "@types/node": { "version": "14.11.2", @@ -656,9 +656,9 @@ "integrity": "sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==" }, "@types/qs": { - "version": "6.9.5", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", - "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==" + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==" }, "@types/range-parser": { "version": "1.2.3", @@ -666,11 +666,11 @@ "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" }, "@types/serve-static": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.6.tgz", - "integrity": "sha512-nuRJmv7jW7VmCVTn+IgYDkkbbDGyIINOeu/G0d74X3lm6E5KfMeQPJhxIt1ayQeQB3cSxvYs1RA/wipYoFB4EA==", + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", + "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", "requires": { - "@types/mime": "*", + "@types/mime": "^1", "@types/node": "*" } }, @@ -708,9 +708,9 @@ }, "dependencies": { "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { "ms": "2.1.2" } @@ -1153,9 +1153,9 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "auth0-js": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.14.0.tgz", - "integrity": "sha512-40gIBUejmYAYse06ck6sxdNO0KU0pX+KDIQsWAkcyFtI0HU6dY5aeHxZfVYkYjtbArKr5s13LuZFdKrUiGyCqQ==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.16.0.tgz", + "integrity": "sha512-I9jECErKZviVPVg0hKfG7URiGV/woyd0JOnh1SKH7Vy4/9n+AkJXgZqF7ayGV5W8sHKJl2aZ3ve3fc50LfR07g==", "requires": { "base64-js": "^1.3.0", "idtoken-verifier": "^2.0.3", @@ -1175,9 +1175,9 @@ } }, "mime": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" }, "ms": { "version": "2.1.2", @@ -1203,9 +1203,12 @@ }, "dependencies": { "qs": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", - "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "requires": { + "side-channel": "^1.0.4" + } } } } @@ -1248,9 +1251,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", - "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==" + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "axios": { "version": "0.19.2", @@ -1441,9 +1444,9 @@ "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" }, "bunyan": { - "version": "1.8.14", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.14.tgz", - "integrity": "sha512-LlahJUxXzZLuw/hetUQJmRgZ1LF6+cr5TPpRj6jf327AsiIq2jhYEH4oqUUkVKTor+9w2BT3oxVwhzE5lw9tcg==", + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", + "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", "requires": { "dtrace-provider": "~0.8", "moment": "^2.19.3", @@ -1882,9 +1885,9 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" }, "core-util-is": { "version": "1.0.2", @@ -3329,9 +3332,9 @@ }, "dependencies": { "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { "ms": "2.1.2" } @@ -3373,9 +3376,9 @@ }, "dependencies": { "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { "ms": "2.1.2" } @@ -4188,9 +4191,9 @@ }, "dependencies": { "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "semver": { "version": "5.7.1", @@ -4237,12 +4240,12 @@ } }, "jwks-rsa": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.11.0.tgz", - "integrity": "sha512-G7ZgXZ3dlGbOUBQwgF+U/SVzOlI9KxJ9Uzp61bue2S5TV0h7c+kJRCl3bEPkC5PVmeu7/h82B3uQALVJMjzt/Q==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.12.3.tgz", + "integrity": "sha512-cFipFDeYYaO9FhhYJcZWX/IyZgc0+g316rcHnDpT2dNRNIE/lMOmWKKqp09TkJoYlNFzrEVODsR4GgXJMgWhnA==", "requires": { "@types/express-jwt": "0.0.42", - "axios": "^0.19.2", + "axios": "^0.21.1", "debug": "^4.1.0", "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", @@ -4253,18 +4256,38 @@ "proxy-from-env": "^1.1.0" }, "dependencies": { + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, + "follow-redirects": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.0.tgz", + "integrity": "sha512-0vRwd7RKQBTt+mgu87mtYeofLFZpTas2S9zY+jIeuLJMNvudIgF52nr19q40HOwH5RrhWIPuj9puybzSJiRrVg==" + }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" } } }, @@ -4615,9 +4638,9 @@ } }, "lru-memoizer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.2.tgz", - "integrity": "sha512-N5L5xlnVcbIinNn/TJ17vHBZwBMt9t7aJDz2n97moWubjNl6VO9Ao2XuAGBBddkYdjrwR9HfzXbT6NfMZXAZ/A==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", "requires": { "lodash.clonedeep": "^4.5.0", "lru-cache": "~4.0.0" @@ -6731,6 +6754,23 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "dependencies": { + "object-inspect": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.2.tgz", + "integrity": "sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA==" + } + } + }, "sigmund": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", @@ -7939,9 +7979,9 @@ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, "yallist": { diff --git a/src/common/helper.js b/src/common/helper.js index 26caf51b..23d31e67 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -88,18 +88,29 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { type: 'nested', properties: { id: { type: 'keyword' }, + xaiId: { type: 'keyword' }, jobCandidateId: { type: 'keyword' }, - googleCalendarId: { type: 'keyword' }, - customMessage: { type: 'text' }, - xaiTemplate: { type: 'keyword' }, + calendarEventId: { type: 'keyword' }, + templateUrl: { type: 'keyword' }, + templateId: { type: 'keyword' }, + templateType: { type: 'keyword' }, + title: { type: 'keyword' }, + locationDetails: { type: 'keyword' }, + duration: { type: 'integer' }, startTimestamp: { type: 'date' }, - attendeesList: [], + endTimestamp: { type: 'date' }, + hostName: { type: 'keyword' }, + hostEmail: { type: 'keyword' }, + guestNames: { type: 'keyword' }, + guestEmails: { type: 'keyword' }, round: { type: 'integer' }, status: { type: 'keyword' }, + rescheduleUrl: { type: 'keyword' }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } + updatedBy: { type: 'keyword' }, + deletedAt: { type: 'date' } } }, createdAt: { type: 'date' }, @@ -1097,6 +1108,22 @@ async function getMemberDetailsByHandles (handles) { return _.get(res.body, 'result.content') } +/** + * Get topcoder member details by handle. + * + * @param {String} handle the user handle + * @returns {Object} the member details + */ + async function getV3MemberDetailsByHandle (handle) { + const token = await getM2MToken() + const res = await request + .get(`${config.TOPCODER_MEMBERS_API}/${handle}`) + .set('Authorization', `Bearer ${token}`) + .set('Accept', 'application/json') + localLogger.debug({ context: 'getV3MemberDetailsByHandle', message: `response body: ${JSON.stringify(res.body)}` }) + return _.get(res.body, 'result.content') +} + /** * Find topcoder members by email. * @@ -1369,17 +1396,17 @@ async function getUserByHandle (userHandle) { } /** - * - * @param {String} string that will be modifed + * + * @param {String} string that will be modifed * @param {*} object of json that would be replaced in string - * @returns + * @returns */ async function substituteStringByObject (string, object) { for (var key in object) { - if (!object.hasOwnProperty(key)) { - continue; + if (!Object.prototype.hasOwnProperty.call(object, key)) { + continue } - string = string.replace(new RegExp("{{" + key + "}}", "g"), object[key]); + string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]) } return string } @@ -1423,6 +1450,7 @@ module.exports = { getAuditM2Muser, checkIsMemberOfProject, getMemberDetailsByHandles, + getV3MemberDetailsByHandle, getMemberDetailsByEmails, createProjectMember, listProjectMembers, diff --git a/src/controllers/InterviewController.js b/src/controllers/InterviewController.js index d9ae8e44..21cceca1 100644 --- a/src/controllers/InterviewController.js +++ b/src/controllers/InterviewController.js @@ -14,6 +14,15 @@ async function getInterviewByRound (req, res) { res.send(await service.getInterviewByRound(req.authUser, jobCandidateId, round, req.query.fromDb)) } +/** + * Get interview by id + * @param req the request + * @param res the response + */ +async function getInterviewById (req, res) { + res.send(await service.getInterviewById(req.authUser, req.params.id, req.query.fromDb)) +} + /** * Request interview * @param req the request @@ -24,13 +33,22 @@ async function requestInterview (req, res) { } /** - * Patch (partially update) interview + * Patch (partially update) interview by round * @param req the request * @param res the response */ -async function partiallyUpdateInterview (req, res) { +async function partiallyUpdateInterviewByRound (req, res) { const { jobCandidateId, round } = req.params - res.send(await service.partiallyUpdateInterview(req.authUser, jobCandidateId, round, req.body)) + res.send(await service.partiallyUpdateInterviewByRound(req.authUser, jobCandidateId, round, req.body)) +} + +/** + * Patch (partially update) interview by id + * @param req the request + * @param res the response + */ +async function partiallyUpdateInterviewById (req, res) { + res.send(await service.partiallyUpdateInterviewById(req.authUser, req.params.id, req.body)) } /** @@ -46,7 +64,9 @@ async function searchInterviews (req, res) { module.exports = { getInterviewByRound, + getInterviewById, requestInterview, - partiallyUpdateInterview, + partiallyUpdateInterviewByRound, + partiallyUpdateInterviewById, searchInterviews } diff --git a/src/eventHandlers/InterviewEventHandler.js b/src/eventHandlers/InterviewEventHandler.js index 11d118b4..5215798d 100644 --- a/src/eventHandlers/InterviewEventHandler.js +++ b/src/eventHandlers/InterviewEventHandler.js @@ -5,9 +5,7 @@ const models = require('../models') // const logger = require('../common/logger') const helper = require('../common/helper') -const { Interviews } = require('../../app-constants') const teamService = require('../services/TeamService') -const _ = require('lodash') /** * Once we request Interview for a JobCandidate, the invitation emails to be sent out. @@ -17,29 +15,20 @@ const _ = require('lodash') */ async function sendInvitationEmail (payload) { const interview = payload.value - // get the Interviewer - const interviewerUsers = await helper.getMemberDetailsByEmails(interview.attendeesList) - .then((members) => _.map(members, (member) => ({ ...member, emailLowerCase: member.email.toLowerCase() }))) - // get job candidate user details + // get customer details via job candidate user const jobCandidate = await models.JobCandidate.findById(interview.jobCandidateId) - const jobCandidateUser = await helper.getUserById(jobCandidate.userId) - const jobCandidateMember = await helper.getUserByHandle(jobCandidateUser.handle) - // get customer details const job = await jobCandidate.getJob() - teamService.sendEmail({}, { template: 'interview-invitation', - cc: [jobCandidateMember.email, ...interview.attendeesList], + cc: [interview.hostEmail, ...interview.guestEmails], data: { - job_candidate_id: interview.jobCandidateId, - interview_round: interview.round, - interviewee_name: `${jobCandidateMember.firstName} ${jobCandidateMember.lastName}`, - interviewer_name: `${interviewerUsers[0].firstName} ${interviewerUsers[0].lastName}`, - xai_template: '/' + interview.xaiTemplate, - additional_interviewers: (interview.attendeesList).join(','), - interview_length: Interviews.XaiTemplate[interview.xaiTemplate], - job_name: job.title, - interviewee_handle: jobCandidateMember.handle + interview_id: interview.id, + interviewee_name: interview.guestNames[0], + interviewer_name: interview.hostName, + xai_template: '/' + interview.templateUrl, + additional_interviewers_name: (interview.guestNames.slice(1)).join(','), + interview_length: interview.duration, + job_name: job.title } }) } diff --git a/src/models/Interview.js b/src/models/Interview.js index 424251a3..23dbb152 100644 --- a/src/models/Interview.js +++ b/src/models/Interview.js @@ -42,24 +42,44 @@ module.exports = (sequelize) => { allowNull: false, defaultValue: Sequelize.UUIDV4 }, + xaiId: { + field: 'xai_id', + type: Sequelize.STRING(255) + }, jobCandidateId: { field: 'job_candidate_id', type: Sequelize.UUID, allowNull: false }, - googleCalendarId: { - field: 'google_calendar_id', + calendarEventId: { + field: 'calendar_event_id', type: Sequelize.STRING(255) }, - customMessage: { - field: 'custom_message', - type: Sequelize.TEXT - }, - xaiTemplate: { - field: 'xai_template', + templateUrl: { + field: 'template_url', type: Sequelize.STRING(255), allowNull: false }, + templateId: { + field: 'template_id', + type: Sequelize.STRING(255) + }, + templateType: { + field: 'template_type', + type: Sequelize.STRING(255) + }, + title: { + field: 'title', + type: Sequelize.STRING(255) + }, + locationDetails: { + field: 'location_details', + type: Sequelize.STRING(255) + }, + duration: { + field: 'duration', + type: Sequelize.INTEGER + }, round: { type: Sequelize.INTEGER, allowNull: false @@ -68,14 +88,34 @@ module.exports = (sequelize) => { field: 'start_timestamp', type: Sequelize.DATE }, - attendeesList: { - field: 'attendees_list', + endTimestamp: { + field: 'end_timestamp', + type: Sequelize.DATE + }, + hostName: { + field: 'host_name', + type: Sequelize.STRING(255) + }, + hostEmail: { + field: 'host_email', + type: Sequelize.STRING(255) + }, + guestNames: { + field: 'guest_names', + type: Sequelize.ARRAY(Sequelize.STRING) + }, + guestEmails: { + field: 'guest_emails', type: Sequelize.ARRAY(Sequelize.STRING) }, status: { type: Sequelize.ENUM(statuses), allowNull: false }, + rescheduleUrl: { + field: 'reschedule_url', + type: Sequelize.STRING(255) + }, createdBy: { field: 'created_by', type: Sequelize.UUID, @@ -102,21 +142,11 @@ module.exports = (sequelize) => { schema: config.DB_SCHEMA_NAME, sequelize, tableName: 'interviews', - paranoid: true, + paranoid: false, deletedAt: 'deletedAt', createdAt: 'createdAt', updatedAt: 'updatedAt', - timestamps: true, - defaultScope: { - attributes: { - exclude: ['deletedAt'] - } - }, - hooks: { - afterCreate: (interview) => { - delete interview.dataValues.deletedAt - } - } + timestamps: true } ) diff --git a/src/routes/InterviewRoutes.js b/src/routes/InterviewRoutes.js index e8f8e960..e4a419ad 100644 --- a/src/routes/InterviewRoutes.js +++ b/src/routes/InterviewRoutes.js @@ -15,7 +15,15 @@ module.exports = { '/jobCandidates/:jobCandidateId/updateInterview/:round': { patch: { controller: 'InterviewController', - method: 'partiallyUpdateInterview', + method: 'partiallyUpdateInterviewByRound', + auth: 'jwt', + scopes: [constants.Scopes.UPDATE_INTERVIEW, constants.Scopes.ALL_INTERVIEW] + } + }, + '/updateInterview/:id': { + patch: { + controller: 'InterviewController', + method: 'partiallyUpdateInterviewById', auth: 'jwt', scopes: [constants.Scopes.UPDATE_INTERVIEW, constants.Scopes.ALL_INTERVIEW] } @@ -35,5 +43,13 @@ module.exports = { auth: 'jwt', scopes: [constants.Scopes.READ_INTERVIEW, constants.Scopes.ALL_INTERVIEW] } + }, + '/getInterview/:id': { + get: { + controller: 'InterviewController', + method: 'getInterviewById', + auth: 'jwt', + scopes: [constants.Scopes.READ_INTERVIEW, constants.Scopes.ALL_INTERVIEW] + } } } diff --git a/src/services/InterviewService.js b/src/services/InterviewService.js index 9d40cfeb..10a065f4 100644 --- a/src/services/InterviewService.js +++ b/src/services/InterviewService.js @@ -4,9 +4,10 @@ const _ = require('lodash') const Joi = require('joi') +const moment = require('moment') const config = require('config') const { Op, ForeignKeyConstraintError } = require('sequelize') -const { v4: uuid } = require('uuid') +const { v4: uuid, validate: uuidValidate } = require('uuid') const { Interviews: InterviewConstants } = require('../../app-constants') const helper = require('../common/helper') const logger = require('../common/logger') @@ -86,7 +87,7 @@ async function getInterviewByRound (currentUser, jobCandidateId, round, fromDb = } } // either ES query failed or `fromDb` is set - fallback to DB - logger.info({ component: 'InterviewService', context: 'getInterview', message: 'try to query db for data' }) + logger.info({ component: 'InterviewService', context: 'getInterviewByRound', message: 'try to query db for data' }) const interview = await Interview.findOne({ where: { jobCandidateId, round } @@ -106,6 +107,103 @@ getInterviewByRound.schema = Joi.object().keys({ fromDb: Joi.boolean() }).required() +/** + * Get interview by id + * @param {Object} currentUser the user who perform this operation. + * @param {String} id the interview or xai id + * @param {Boolean} fromDb flag if query db for data or not + * @returns {Object} the interview + */ +async function getInterviewById (currentUser, id, fromDb = false) { + if (!fromDb) { + try { + // construct query for nested search + const esQueryBody = { + _source: false, + query: { + nested: { + path: 'interviews', + query: { + bool: { + should: [] + } + }, + inner_hits: {} + } + } + } + // add filtering terms + // interviewId + esQueryBody.query.nested.query.bool.should.push({ + term: { + 'interviews.id': { + value: id + } + } + }) + // xaiId + esQueryBody.query.nested.query.bool.should.push({ + term: { + 'interviews.xaiId': { + value: id + } + } + }) + // search + const { body } = await esClient.search({ + index: config.esConfig.ES_INDEX_JOB_CANDIDATE, + body: esQueryBody + }) + // extract inner interview hit from body - there's always one jobCandidate & interview hit as we search with IDs + const interview = _.get(body, 'hits.hits[0].inner_hits.interviews.hits.hits[0]._source') + if (interview) { + // check permission before returning + await ensureUserIsPermitted(currentUser, interview.jobCandidateId) + return interview + } + // if reaches here, the interview with this IDs is not found + throw new errors.NotFoundError(`Interview doesn't exist with id/xaiId: ${id}`) + } catch (err) { + logger.logFullError(err, { component: 'InterviewService', context: 'getInterviewById' }) + throw err + } + } + // either ES query failed or `fromDb` is set - fallback to DB + logger.info({ component: 'InterviewService', context: 'getInterviewById', message: 'try to query db for data' }) + var interview + if (uuidValidate(id)) { + interview = await Interview.findOne({ + where: { + [Op.or]: [ + { id } + ] + } + }) + } else { + interview = await Interview.findOne({ + where: { + [Op.or]: [ + { xaiId: id } + ] + } + }) + } + // throw NotFound error if doesn't exist + if (!!interview !== true) { + throw new errors.NotFoundError(`Interview doesn't exist with id/xaiId: ${id}`) + } + // check permission before returning + await ensureUserIsPermitted(currentUser, interview.jobCandidateId) + + return interview.dataValues +} + +getInterviewById.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.string().required(), + fromDb: Joi.boolean() +}).required() + /** * Request interview * @param {Object} currentUser the user who perform this operation @@ -117,10 +215,6 @@ async function requestInterview (currentUser, jobCandidateId, interview) { // check permission await ensureUserIsPermitted(currentUser, jobCandidateId) - interview.id = uuid() - interview.jobCandidateId = jobCandidateId - interview.createdBy = await helper.getUserId(currentUser.userId) - // find the round count const round = await Interview.count({ where: { jobCandidateId } @@ -130,7 +224,26 @@ async function requestInterview (currentUser, jobCandidateId, interview) { if (round >= InterviewConstants.MaxAllowedCount) { throw new errors.ConflictError(`You've reached the maximum allowed number (${InterviewConstants.MaxAllowedCount}) of interviews for this candidate.`) } + + // get job candidate user details + const jobCandidate = await models.JobCandidate.findById(jobCandidateId) + const jobCandidateUser = await helper.getUserById(jobCandidate.userId) + const jobCandidateMember = await helper.getUserByHandle(jobCandidateUser.handle) + // pre-populate fields + interview.id = uuid() + interview.jobCandidateId = jobCandidateId interview.round = round + 1 + interview.duration = InterviewConstants.XaiTemplate[interview.templateUrl] + interview.createdBy = await helper.getUserId(currentUser.userId) + interview.guestEmails = [jobCandidateMember.email, ...interview.guestEmails] + // pre-populate hostName & guestNames + const hostMembers = await helper.getMemberDetailsByEmails([interview.hostEmail]) + const guestMembers = await helper.getMemberDetailsByEmails(interview.guestEmails) + interview.hostName = `${hostMembers[0].firstName} ${hostMembers[0].lastName}` + interview.guestNames = _.map(interview.guestEmails, (guestEmail) => { + var foundGuestMember = _.find(guestMembers, function(guestMember) { return guestEmail == guestMember.email }); + return (foundGuestMember != undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split("@")[0] + }) try { // create the interview @@ -157,78 +270,177 @@ requestInterview.schema = Joi.object().keys({ currentUser: Joi.object().required(), jobCandidateId: Joi.string().uuid().required(), interview: Joi.object().keys({ - googleCalendarId: Joi.string().allow(null), - customMessage: Joi.string().allow(null), - xaiTemplate: Joi.xaiTemplate().required(), - attendeesList: Joi.array().items(Joi.string().email()).allow(null), + calendarEventId: Joi.string().allow(null), + templateUrl: Joi.xaiTemplate().required(), + hostEmail: Joi.string().email().required(), + guestEmails: Joi.array().items(Joi.string().email()).default([]), status: Joi.interviewStatus().default(InterviewConstants.Status.Scheduling) }).required() }).required() /** - * Patch (partially update) interview - * @param {Object} currentUser the user who perform this operation - * @param {String} jobCandidateId the job candidate id - * @param {Number} round the interview round - * @param {Object} data object containing patched fields - * @returns {Object} the patched interview object + * Updates interview + * + * @param {Object} currentUser user who performs the operation + * @param {Object} interview the existing interview object + * @param {Object} data object containing updated fields + * @returns {Object} updated interview */ -async function partiallyUpdateInterview (currentUser, jobCandidateId, round, data) { - // check permission - await ensureUserIsPermitted(currentUser, jobCandidateId) - - const interview = await Interview.findOne({ - where: { - jobCandidateId, round - } - }) - // throw NotFound error if doesn't exist - if (!!interview !== true) { - throw new errors.NotFoundError(`Interview doesn't exist with jobCandidateId: ${jobCandidateId} and round: ${round}`) - } - - const oldValue = interview.toJSON() - +async function partiallyUpdateInterview (currentUser, interview, data) { // only status can be updated for Completed interviews - if (oldValue.status === InterviewConstants.Status.Completed) { + if (interview.status === InterviewConstants.Status.Completed) { const updatedFields = _.keys(data) if (updatedFields.length !== 1 || !_.includes(updatedFields, 'status')) { throw new errors.BadRequestError('Only the "status" can be updated for Completed interviews.') } } + // automatically set endTimestamp if startTimestamp is provided + if (data.startTimestamp && !!data.endTimestamp !== true) { + data.endTimestamp = moment(data.startTimestamp).add(interview.duration, 'minutes').toDate() + } + data.updatedBy = await helper.getUserId(currentUser.userId) try { const updated = await interview.update(data) - await helper.postEvent(config.TAAS_INTERVIEW_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) + await helper.postEvent(config.TAAS_INTERVIEW_UPDATE_TOPIC, updated.toJSON(), { oldValue: interview.toJSON() }) return updated.dataValues } catch (err) { // gracefully handle if one of the common sequelize errors - handleSequelizeError(err, jobCandidateId) + handleSequelizeError(err, interview.jobCandidateId) // if reaches here, it's not one of the common errors handled in `handleSequelizeError` throw err } } -partiallyUpdateInterview.schema = Joi.object().keys({ +/** + * Patch (partially update) interview by round + * @param {Object} currentUser the user who perform this operation + * @param {String} jobCandidateId the job candidate id + * @param {Number} round the interview round + * @param {Object} data object containing patched fields + * @returns {Object} the patched interview object + */ +async function partiallyUpdateInterviewByRound (currentUser, jobCandidateId, round, data) { + const interview = await Interview.findOne({ + where: { + jobCandidateId, round + } + }) + // throw NotFound error if doesn't exist + if (!!interview !== true) { + throw new errors.NotFoundError(`Interview doesn't exist with jobCandidateId: ${jobCandidateId} and round: ${round}`) + } + // check permission + await ensureUserIsPermitted(currentUser, jobCandidateId) + + return await partiallyUpdateInterview(currentUser, interview, data) +} + +partiallyUpdateInterviewByRound.schema = Joi.object().keys({ currentUser: Joi.object().required(), jobCandidateId: Joi.string().uuid().required(), round: Joi.number().integer().positive().required(), data: Joi.object().keys({ - googleCalendarId: Joi.string().when('status', { + xaiId: Joi.string().allow(null), + calendarEventId: Joi.string().when('status', { is: [InterviewConstants.Status.Scheduled, InterviewConstants.Status.Rescheduled], then: Joi.required(), otherwise: Joi.allow(null) }), - customMessage: Joi.string().allow(null), - xaiTemplate: Joi.xaiTemplate(), + templateUrl: Joi.xaiTemplate(), + templateId: Joi.string().allow(null), + templateType: Joi.string().allow(null), + title: Joi.string().allow(null), + locationDetails: Joi.string().allow(null), startTimestamp: Joi.date().greater('now').when('status', { is: [InterviewConstants.Status.Scheduled, InterviewConstants.Status.Rescheduled], then: Joi.required(), otherwise: Joi.allow(null) }), - attendeesList: Joi.array().items(Joi.string().email()).allow(null), - status: Joi.interviewStatus() + endTimestamp: Joi.date().greater(Joi.ref('startTimestamp')).when('status', { + is: [InterviewConstants.Status.Scheduled, InterviewConstants.Status.Rescheduled], + then: Joi.required(), + otherwise: Joi.allow(null) + }), + hostName: Joi.string(), + hostEmail: Joi.string().email(), + guestNames: Joi.array().items(Joi.string()).allow(null), + guestEmails: Joi.array().items(Joi.string().email()).allow(null), + status: Joi.interviewStatus(), + rescheduleUrl: Joi.string().allow(null), + deletedAt: Joi.date().allow(null) + }).required().min(1) // at least one key - i.e. don't allow empty object +}).required() + +/** + * Patch (partially update) interview by id + * @param {Object} currentUser the user who perform this operation + * @param {String} id the interview or x.ai meeting id + * @param {Object} data object containing patched fields + * @returns {Object} the patched interview object + */ +async function partiallyUpdateInterviewById (currentUser, id, data) { + var interview + if (uuidValidate(id)) { + interview = await Interview.findOne({ + where: { + [Op.or]: [ + { id } + ] + } + }) + } else { + interview = await Interview.findOne({ + where: { + [Op.or]: [ + { xaiId: id } + ] + } + }) + } + // throw NotFound error if doesn't exist + if (!!interview !== true) { + throw new errors.NotFoundError(`Interview doesn't exist with id/xaiId: ${id}`) + } + // check permission + await ensureUserIsPermitted(currentUser, interview.jobCandidateId) + + return await partiallyUpdateInterview(currentUser, interview, data) +} + +partiallyUpdateInterviewById.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.string().required(), + data: Joi.object().keys({ + xaiId: Joi.string().required(), + calendarEventId: Joi.string().when('status', { + is: [InterviewConstants.Status.Scheduled, InterviewConstants.Status.Rescheduled], + then: Joi.required(), + otherwise: Joi.allow(null) + }), + templateUrl: Joi.xaiTemplate(), + templateId: Joi.string().allow(null), + templateType: Joi.string().allow(null), + title: Joi.string().allow(null), + locationDetails: Joi.string().allow(null), + startTimestamp: Joi.date().greater('now').when('status', { + is: [InterviewConstants.Status.Scheduled, InterviewConstants.Status.Rescheduled], + then: Joi.required(), + otherwise: Joi.allow(null) + }), + endTimestamp: Joi.date().greater(Joi.ref('startTimestamp')).when('status', { + is: [InterviewConstants.Status.Scheduled, InterviewConstants.Status.Rescheduled], + then: Joi.required(), + otherwise: Joi.allow(null) + }), + hostName: Joi.string(), + hostEmail: Joi.string().email(), + guestNames: Joi.array().items(Joi.string()).allow(null), + guestEmails: Joi.array().items(Joi.string().email()).allow(null), + status: Joi.interviewStatus(), + rescheduleUrl: Joi.string().allow(null), + deletedAt: Joi.date().allow(null) }).required().min(1) // at least one key - i.e. don't allow empty object }).required() @@ -395,8 +607,10 @@ async function updateCompletedInterviews () { module.exports = { getInterviewByRound, + getInterviewById, requestInterview, - partiallyUpdateInterview, + partiallyUpdateInterviewByRound, + partiallyUpdateInterviewById, searchInterviews, updateCompletedInterviews } diff --git a/src/services/PaymentService.js b/src/services/PaymentService.js index 7d714f75..3143b936 100644 --- a/src/services/PaymentService.js +++ b/src/services/PaymentService.js @@ -39,7 +39,7 @@ async function createPayment (options) { const challengeId = await createChallenge(options, token) await addResourceToChallenge(challengeId, options.userHandle, token) await activateChallenge(challengeId, token) - const completedChallenge = await closeChallenge(challengeId, token) + const completedChallenge = await closeChallenge(challengeId, options.userHandle, token) return completedChallenge } @@ -146,14 +146,21 @@ async function activateChallenge (id, token) { /** * closes the topcoder challenge * @param {String} id the challenge id + * @param {String} userHandle the user handle * @param {String} token m2m token * @returns {Object} the closed challenge */ -async function closeChallenge (id, token) { +async function closeChallenge (id, userHandle, token) { localLogger.info({ context: 'closeChallenge', message: `Closing challenge ${id}` }) try { + const { userId } = await helper.getV3MemberDetailsByHandle(userHandle) const body = { - status: constants.ChallengeStatus.COMPLETED + status: constants.ChallengeStatus.COMPLETED, + winners: [{ + userId, + handle: userHandle, + placement: 1 + }] } const response = await helper.updateChallenge(id, body, token) localLogger.info({ context: 'closeChallenge', message: `Challenge ${id} is closed successfully.` }) diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 2f9b32c5..a1432fd1 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -325,7 +325,7 @@ async function sendEmail (currentUser, data) { subject: data.subject || template.subject, body: data.body || template.body } - for(var key in subjectBody) { + for (const key in subjectBody) { subjectBody[key] = await helper.substituteStringByObject(subjectBody[key], data.data) } const emailData = {