diff --git a/app-constants.js b/app-constants.js index 9b577728..d24764b5 100644 --- a/app-constants.js +++ b/app-constants.js @@ -34,6 +34,8 @@ const Scopes = { ALL_RESOURCE_BOOKING: 'all:taas-resourceBookings', // taas-team READ_TAAS_TEAM: 'read:taas-teams', + CREATE_ROLE_SEARCH_REQUEST: 'create:taas-roleSearchRequests', + CREATE_TAAS_TEAM: 'create:taas-teams', // work period READ_WORK_PERIOD: 'read:taas-workPeriods', CREATE_WORK_PERIOD: 'create:taas-workPeriods', diff --git a/config/default.js b/config/default.js index 93f42e20..6115cabf 100644 --- a/config/default.js +++ b/config/default.js @@ -171,5 +171,9 @@ module.exports = { DEFAULT_TIMELINE_TEMPLATE_ID: process.env.DEFAULT_TIMELINE_TEMPLATE_ID || '53a307ce-b4b3-4d6f-b9a1-3741a58f77e6', DEFAULT_TRACK_ID: process.env.DEFAULT_TRACK_ID || '9b6fc876-f4d9-4ccb-9dfd-419247628825', - PAYMENT_PROCESSING_SWITCH: process.env.PAYMENT_PROCESSING_SWITCH || 'OFF' + PAYMENT_PROCESSING_SWITCH: process.env.PAYMENT_PROCESSING_SWITCH || 'OFF', + // the minimum matching rate when searching roles by skills + ROLE_MATCHING_RATE: process.env.ROLE_MATCHING_RATE || 0.70, + // member groups representing Wipro or TopCoder employee + INTERNAL_MEMBER_GROUPS: process.env.INTERNAL_MEMBER_GROUPS || ['20000000', '20000001', '20000003', '20000010', '20000015'] } diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index c1233474..352da4d6 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "2c9dbe94-39f9-4a01-97e4-70f781fc1364", + "_postman_id": "18310e1b-429d-49db-8555-f4a54404271f", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -19336,6 +19336,1695 @@ { "name": "Taas Teams", "item": [ + { + "name": "Before Start", + "item": [ + { + "name": "create role 1", + "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(\"roleIdForTeam-1\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer\",\n \"description\": \"A DevOps engineer introduces processes, tools, and methodologies to balance needs throughout the software development life cycle, from coding and deployment, to maintenance and updates.\",\n \"listOfSkills\": [\n \"Dropwizard\",\n \"NGINX\",\n \"Machine Learning\",\n \"Force.com\",\n \"User Interface (Ui)\",\n \"Photoshop\",\n \"appcelerator\",\n \"Flux\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n }\n ],\n \"numberOfMembers\": 10,\n \"numberOfMembersAvailable\": 8,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-roles", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-roles" + ] + } + }, + "response": [] + }, + { + "name": "create role 2", + "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(\"roleIdForTeam-2\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Angular Developer\",\n \"description\": \"Angular is an open-source, client-side framework based on TypeScript and designed for building web applications.\",\n \"listOfSkills\": [\n \"Database\",\n \"Winforms\",\n \"User Interface (Ui)\",\n \"Photoshop\",\n \"Dropwizard\",\n \"NGINX\",\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 55,\n \"inCountry\": 20,\n \"offShore\": 10,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n }\n ],\n \"numberOfMembers\": 10,\n \"numberOfMembersAvailable\": 8,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-roles", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-roles" + ] + } + }, + "response": [] + }, + { + "name": "create role 3", + "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(\"roleIdForTeam-3\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Salesforce Developer\",\n \"description\": \"A Salesforce developer is a programmer who builds Salesforce applications across various PaaS (Platform as a Service) platforms.\",\n \"listOfSkills\": [\n \"Docker\",\n \".NET\",\n \"appcelerator\",\n \"Flux\",\n \"Database\",\n \"Winforms\",\n \"NGINX\",\n \"Machine Learning\"\n ],\n \"rates\": [\n {\n \"global\": 40,\n \"inCountry\": 20,\n \"offShore\": 10,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5,\n \"rate30Global\": 20,\n \"rate30InCountry\": 15,\n \"rate30OffShore\": 35,\n \"rate20Global\": 20,\n \"rate20InCountry\": 15,\n \"rate20OffShore\": 35\n }\n ],\n \"numberOfMembers\": 10,\n \"numberOfMembersAvailable\": 6,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-roles", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-roles" + ] + } + }, + "response": [] + }, + { + "name": "create role Niche", + "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": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Niche\",\n \"rates\": [\n {\n \"global\": 10,\n \"inCountry\": 10,\n \"offShore\": 10\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-roles", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-roles" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Create Role Search", + "item": [ + { + "name": "send request with administrator using roleId", + "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(\"roleSearchRequestId1-1\", response.roleSearchRequestId);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"roleId\": \"{{roleIdForTeam-1}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with administrator using skills", + "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(\"roleSearchRequestId1-2\", response.roleSearchRequestId);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"skills\": [\r\n \"56f46882-26f3-4c39-966d-912cccea0119\",\r\n \"536865d3-e7c7-4675-b119-6df8bf411624\",\r\n \"bd417c10-d81a-45b6-85a9-d79efe86b9bb\",\r\n \"4fce6ced-3610-443c-92eb-3f6d76b34f5c\"\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with administrator using description", + "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(\"roleSearchRequestId1-3\", response.roleSearchRequestId);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"jobDescription\": \"Should have these skills: Machine Learning, Dropwizard, NGINX, appcelerator\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with user using roleId", + "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(\"roleSearchRequestId2-1\", response.roleSearchRequestId);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_member_test_2}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"roleId\": \"{{roleIdForTeam-2}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with user using skills", + "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(\"roleSearchRequestId2-2\", response.roleSearchRequestId);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_member_test_2}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"skills\": [\r\n \"56f46882-26f3-4c39-966d-912cccea0119\",\r\n \"536865d3-e7c7-4675-b119-6df8bf411624\",\r\n \"bd417c10-d81a-45b6-85a9-d79efe86b9bb\",\r\n \"4fce6ced-3610-443c-92eb-3f6d76b34f5c\"\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with user using description", + "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(\"roleSearchRequestId2-3\", response.roleSearchRequestId);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_member_test_2}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"jobDescription\": \"Should have these skills: Machine Learning, Dropwizard, NGINX, appcelerator\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with m2m using roleId", + "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(\"roleSearchRequestId3-1\", response.roleSearchRequestId);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_m2m_create_roleSearchRequests}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"roleId\": \"{{roleIdForTeam-3}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with m2m using skills", + "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(\"roleSearchRequestId3-2\", response.roleSearchRequestId);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_m2m_create_roleSearchRequests}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"skills\": [\r\n \"56f46882-26f3-4c39-966d-912cccea0119\",\r\n \"536865d3-e7c7-4675-b119-6df8bf411624\",\r\n \"bd417c10-d81a-45b6-85a9-d79efe86b9bb\",\r\n \"4fce6ced-3610-443c-92eb-3f6d76b34f5c\"\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with m2m using description", + "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(\"roleSearchRequestId3-3\", response.roleSearchRequestId);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_m2m_create_roleSearchRequests}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"jobDescription\": \"Should have these skills: Machine Learning, Dropwizard, NGINX, appcelerator\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with invalid token", + "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(\"Invalid Token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer invalid_token", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"jobDescription\": \"Should have these skills: Machine Learning, Dropwizard, NGINX, appcelerator\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with missing field", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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\\\" must have at least 1 key\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with invalid field 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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.roleId\\\" must be a valid GUID\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"roleId\": \"000\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with invalid field 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 404', function () {\r", + " pm.response.to.have.status(404);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"id: 00000000-0000-0000-0000-000000000000 \\\"Role\\\" doesn't exists.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"roleId\": \"00000000-0000-0000-0000-000000000000\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with invalid field 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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.skills\\\" must be an array\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"skills\": \"00000000-0000-0000-0000-000000000000\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with invalid field 4", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"Invalid skills: [00000000-0000-0000-0000-000000000000]\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"skills\": [\r\n \"00000000-0000-0000-0000-000000000000\"\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + }, + { + "name": "send request with not matching skills", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + " const response = pm.response.json()\r", + " pm.expect(response.name).to.eq(\"Niche\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"skills\": [\r\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\r\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\r\n \"cbac57a3-7180-4316-8769-73af64893158\",\r\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/sendRoleSearchRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "sendRoleSearchRequest" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Create Team", + "item": [ + { + "name": "create team with administrator", + "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(\"createTeamProjectId-1\", response.projectId);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"teamName\": \"Submit Team Test 1\",\r\n \"teamDescription\":\"Submit Team Test 1 description\",\r\n \"positions\": [\r\n {\r\n \"roleName\": \"Dev Ops Engineer\",\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId1-1}}\",\r\n \"numberOfResources\": 10,\r\n \"durationWeeks\": 7,\r\n \"startMonth\": \"2021-06-15\"\r\n },\r\n {\r\n \"roleName\": \"Salesforce Developer\",\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId1-2}}\",\r\n \"numberOfResources\": 5,\r\n \"durationWeeks\": 5,\r\n \"startMonth\": \"2021-06-15\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/submitTeamRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "submitTeamRequest" + ] + } + }, + "response": [] + }, + { + "name": "get created jobs 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobs?projectId={{createTeamProjectId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "jobs" + ], + "query": [ + { + "key": "projectId", + "value": "{{createTeamProjectId-1}}" + } + ] + } + }, + "response": [] + }, + { + "name": "create team with user", + "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(\"createTeamProjectId-2\", response.projectId);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_member_test_2}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"teamName\": \"Submit Team Test 2\",\r\n \"teamDescription\":\"Submit Team Test 2 description\",\r\n \"positions\": [\r\n {\r\n \"roleName\": \"Angular Developer\",\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId2-1}}\",\r\n \"numberOfResources\": 10,\r\n \"durationWeeks\": 7,\r\n \"startMonth\": \"2021-06-15\"\r\n },\r\n {\r\n \"roleName\": \"Dev Ops Engineer\",\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId2-3}}\",\r\n \"numberOfResources\": 5,\r\n \"durationWeeks\": 5,\r\n \"startMonth\": \"2021-06-15\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/submitTeamRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "submitTeamRequest" + ] + } + }, + "response": [] + }, + { + "name": "get created jobs 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobs?projectId={{createTeamProjectId-2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "jobs" + ], + "query": [ + { + "key": "projectId", + "value": "{{createTeamProjectId-2}}" + } + ] + } + }, + "response": [] + }, + { + "name": "create team with m2m", + "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(\"createTeamProjectId-3\", response.projectId);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_m2m_create_teams}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"teamName\": \"Submit Team Test 3\",\r\n \"teamDescription\":\"Submit Team Test 3 description\",\r\n \"positions\": [\r\n {\r\n \"roleName\": \"Salesforce Developer\",\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId3-2}}\",\r\n \"numberOfResources\": 10,\r\n \"durationWeeks\": 7,\r\n \"startMonth\": \"2021-06-15\"\r\n },\r\n {\r\n \"roleName\": \"Dev Ops Engineer\",\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId3-3}}\",\r\n \"numberOfResources\": 5,\r\n \"durationWeeks\": 5,\r\n \"startMonth\": \"2021-06-15\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/submitTeamRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "submitTeamRequest" + ] + } + }, + "response": [] + }, + { + "name": "get created jobs 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobs?projectId={{createTeamProjectId-3}}", + "host": [ + "{{URL}}" + ], + "path": [ + "jobs" + ], + "query": [ + { + "key": "projectId", + "value": "{{createTeamProjectId-3}}" + } + ] + } + }, + "response": [] + }, + { + "name": "create team with invalid token", + "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(\"Invalid Token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer invalid token", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"teamName\": \"Submit Team Test 1\",\r\n \"teamDescription\":\"Submit Team Test 1 description\",\r\n \"positions\": [\r\n {\r\n \"roleName\": \"Dev Ops Engineer\",\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId1-1}}\",\r\n \"numberOfResources\": 10,\r\n \"durationWeeks\": 7,\r\n \"startMonth\": \"2021-06-15\"\r\n },\r\n {\r\n \"roleName\": \"Salesforce Developer\",\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId1-2}}\",\r\n \"numberOfResources\": 5,\r\n \"durationWeeks\": 5,\r\n \"startMonth\": \"2021-06-15\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/submitTeamRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "submitTeamRequest" + ] + } + }, + "response": [] + }, + { + "name": "create team with missing field 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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.teamName\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"teamDescription\":\"Submit Team Test 1 description\",\r\n \"positions\": [\r\n {\r\n \"roleName\": \"Dev Ops Engineer\",\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId1-1}}\",\r\n \"numberOfResources\": 10,\r\n \"durationWeeks\": 7,\r\n \"startMonth\": \"2021-06-15\"\r\n },\r\n {\r\n \"roleName\": \"Salesforce Developer\",\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId1-2}}\",\r\n \"numberOfResources\": 5,\r\n \"durationWeeks\": 5,\r\n \"startMonth\": \"2021-06-15\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/submitTeamRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "submitTeamRequest" + ] + } + }, + "response": [] + }, + { + "name": "create team with missing field 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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.positions\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"teamName\": \"Submit Team Test 1\",\r\n \"teamDescription\":\"Submit Team Test 1 description\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/submitTeamRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "submitTeamRequest" + ] + } + }, + "response": [] + }, + { + "name": "create team with missing field 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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.positions[0].roleName\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"teamName\": \"Submit Team Test 1\",\r\n \"teamDescription\":\"Submit Team Test 1 description\",\r\n \"positions\": [\r\n {\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId1-1}}\",\r\n \"numberOfResources\": 10,\r\n \"durationWeeks\": 7,\r\n \"startMonth\": \"2021-06-15\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/submitTeamRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "submitTeamRequest" + ] + } + }, + "response": [] + }, + { + "name": "create team with missing field 4", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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.positions[0].roleSearchRequestId\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"teamName\": \"Submit Team Test 1\",\r\n \"teamDescription\":\"Submit Team Test 1 description\",\r\n \"positions\": [\r\n {\r\n \"roleName\": \"Dev Ops Engineer\",\r\n \"numberOfResources\": 10,\r\n \"durationWeeks\": 7,\r\n \"startMonth\": \"2021-06-15\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/submitTeamRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "submitTeamRequest" + ] + } + }, + "response": [] + }, + { + "name": "create team with missing field 5", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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.positions[0].numberOfResources\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"teamName\": \"Submit Team Test 1\",\r\n \"teamDescription\":\"Submit Team Test 1 description\",\r\n \"positions\": [\r\n {\r\n \"roleName\": \"Dev Ops Engineer\",\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId1-1}}\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/submitTeamRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "submitTeamRequest" + ] + } + }, + "response": [] + }, + { + "name": "create team with invalid field 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 404', function () {\r", + " pm.response.to.have.status(404);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"id: 00000000-0000-0000-0000-000000000000 \\\"RoleSearchRequest\\\" doesn't exists.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"teamName\": \"Submit Team Test 1\",\r\n \"teamDescription\":\"Submit Team Test 1 description\",\r\n \"positions\": [\r\n {\r\n \"roleName\": \"Dev Ops Engineer\",\r\n \"roleSearchRequestId\": \"00000000-0000-0000-0000-000000000000\",\r\n \"numberOfResources\": 10,\r\n \"durationWeeks\": 7,\r\n \"startMonth\": \"2021-06-15\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/submitTeamRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "submitTeamRequest" + ] + } + }, + "response": [] + }, + { + "name": "create team with invalid field 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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.positions\\\" must be an array\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_administrator}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"teamName\": \"Submit Team Test 1\",\r\n \"teamDescription\":\"Submit Team Test 1 description\",\r\n \"positions\":\r\n {\r\n \"roleName\": \"Dev Ops Engineer\",\r\n \"roleSearchRequestId\": \"{{roleSearchRequestId1-1}}\",\r\n \"numberOfResources\": 10,\r\n \"durationWeeks\": 7,\r\n \"startMonth\": \"2021-06-15\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/submitTeamRequest", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "submitTeamRequest" + ] + } + }, + "response": [] + } + ] + }, { "name": "GET /taas-teams", "request": { @@ -32845,4 +34534,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1d036ce6..2b7069aa 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3192,10 +3192,8 @@ paths: content: application/json: schema: - type : array - items : { - $ref: "#/components/schemas/SkillItem" - } + type: array + items: { $ref: "#/components/schemas/SkillItem" } "400": description: Bad request content: @@ -3306,7 +3304,105 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /taas-roles/new: + /taas-teams/sendRoleSearchRequest: + post: + tags: + - Teams + description: | + Perform a role search operaion + + **Authorization** Any Topcoder user with valid token is allowed. For not logged users Topcoder m2m token with create:taas-roleSearchRequests scope is allowed. + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RoleSearchRequestBody" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/RoleSearchResponse" + "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" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /taas-teams/submitTeamRequest: + post: + tags: + - Teams + description: | + Creates new Team in persistence and new project that will source this team in Connect. + + **Authorization** Any Topcoder user with valid token is allowed. For not logged users Topcoder m2m token with create:taas-teams scope is allowed. + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SubmitTeamRequestBody" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/SubmitTeamResponse" + "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" + "409": + description: Conflict + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /taas-roles: post: tags: - Roles @@ -3352,7 +3448,6 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /taas-roles: get: tags: - Roles @@ -5183,6 +5278,97 @@ components: type: string description: "the email of a member" example: "xxx@xxx.com" + RoleSearchRequestBody: + anyOf: + - type: object + required: + - roleId + properties: + roleId: + type: string + format: uuid + description: "The role id." + - type: object + required: + - jobDescription + properties: + jobDescription: + type: string + description: "The description of the job." + - type: object + required: + - skills + properties: + skills: + type: array + description: "The array of skill ids." + items: + type: string + format: uuid + description: "The skill id" + + RoleSearchResponse: + allOf: + - $ref: "#/components/schemas/Role" + - type: object + required: + - roleSearchRequestId + - isExternalMember + properties: + roleSearchRequestId: + type: string + format: uuid + description: "The role search request id." + isExternalMember: + type: boolean + description: "Is the user external member" + SubmitTeamRequestBody: + properties: + teamName: + type: string + description: "The name of the team" + teamDescription: + type: string + description: "The description of the team" + positions: + type: array + description: "The array of positions" + items: + type: object + required: + - roleName + - roleSearchRequestId + - numberOfResources + properties: + roleName: + type: string + description: "The name of the role" + roleSearchRequestId: + type: string + format: uuid + description: "The id of roleSearchRequest" + numberOfResources: + type: integer + example: 10 + minimum: 1 + description: "The number of needed resources" + durationWeeks: + type: integer + example: 5 + minimum: 1 + description: "The amount of time in weeks" + startMonth: + type: string + format: date-time + description: "The start day of the job" + SubmitTeamResponse: + required: + - projectId + properties: + projectId: + type: string + format: uuid + description: "The id of created project" Role: required: - id diff --git a/docs/topcoder-bookings.postman_environment.json b/docs/topcoder-bookings.postman_environment.json index 20b81db6..461ac18c 100644 --- a/docs/topcoder-bookings.postman_environment.json +++ b/docs/topcoder-bookings.postman_environment.json @@ -1,5 +1,5 @@ { - "id": "0ce42def-1c70-4c24-8986-914caa57f3c8", + "id": "796b909b-aac8-4261-8bf5-ea9004c73353", "name": "topcoder-bookings", "values": [ { @@ -416,9 +416,114 @@ "key": "roleId-3", "value": "", "enabled": true + }, + { + "key": "token_member_test_2", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik5VSkZORGd4UlRVME5EWTBOVVkzTlRkR05qTXlRamxETmpOQk5UYzVRVUV3UlRFeU56TTJRUSJ9.eyJodHRwczovL3RvcGNvZGVyLWRldi5jb20vcm9sZXMiOlsiVG9wY29kZXIgVXNlciJdLCJodHRwczovL3RvcGNvZGVyLWRldi5jb20vdXNlcklkIjoiODg3NzQ2MzQiLCJodHRwczovL3RvcGNvZGVyLWRldi5jb20vaGFuZGxlIjoiaXNiaWxpciIsImh0dHBzOi8vdG9wY29kZXItZGV2LmNvbS91c2VyX2lkIjoiYXV0aDB8ODg3NzQ2MzQiLCJodHRwczovL3RvcGNvZGVyLWRldi5jb20vdGNzc28iOiI0MDE1Mjg1Nnw4MTM0ZjQ4ZWJlMTFhODQ4YTM3NTllNWVmOWU5MmYyMTQ2OTJlMjExMzA0MGM4MmI1ZDhmNTgxYzZkZmNjYzg4IiwiaHR0cHM6Ly90b3Bjb2Rlci1kZXYuY29tL2FjdGl2ZSI6dHJ1ZSwibmlja25hbWUiOiJpc2JpbGlyIiwibmFtZSI6InZpa2FzLmFnYXJ3YWwrcHNoYWhfbWFuYWdlckB0b3Bjb2Rlci5jb20iLCJwaWN0dXJlIjoiaHR0cHM6Ly9zLmdyYXZhdGFyLmNvbS9hdmF0YXIvOTJhZmIyZjBlZDUyZmRmYWUxZjM3MTAyMWFlNjUwMTM_cz00ODAmcj1wZyZkPWh0dHBzJTNBJTJGJTJGY2RuLmF1dGgwLmNvbSUyRmF2YXRhcnMlMkZ2aS5wbmciLCJ1cGRhdGVkX2F0IjoiMjAyMC0xMC0yNFQwODoyODoyNC4xODRaIiwiZW1haWwiOiJ2aWthcy5hZ2Fyd2FsK3BzaGFoX21hbmFnZXJAdG9wY29kZXIuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlzcyI6Imh0dHBzOi8vYXV0aC50b3Bjb2Rlci1kZXYuY29tLyIsInN1YiI6ImF1dGgwfDQwMTUyODU2IiwiYXVkIjoiQlhXWFVXbmlsVlVQZE4wMXQyU2UyOVR3MlpZTkdadkgiLCJpYXQiOjE2MDM1NDMzMzgsImV4cCI6MzMxNjA0NTI3MzgsIm5vbmNlIjoiUjFBMmN6WXVWVFptYmpaSFJHOTJWbDlEU1VKNlVsbHZRWGMzUkhoNVMzWldkV1pEY0ROWE1FWjFYdz09In0.yM5SFG61TH9pSa1NhfL6Do2YwfbLrflaKnyAchdiq94", + "enabled": true + }, + { + "key": "token_m2m_create_roleSearchRequests", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6dGFhcy1yb2xlU2VhcmNoUmVxdWVzdHMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.Gix3XnsXAJiJQOkHeFYUfjPCPY29MR4ZkwZG8LJTZ6E", + "enabled": true + }, + { + "key": "token_m2m_create_teams", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6dGFhcy10ZWFtcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.7XNO80PSZAQF97P-WsGEWc49S9XAzAuS0fZPuMGnzeo", + "enabled": true + }, + { + "key": "roleIdForTeam-1", + "value": "", + "enabled": true + }, + { + "key": "roleIdForTeam-2", + "value": "", + "enabled": true + }, + { + "key": "roleIdForTeam-3", + "value": "", + "enabled": true + }, + { + "key": "roleSearchRequestId-1", + "value": "", + "enabled": true + }, + { + "key": "roleSearchRequestId-2", + "value": "", + "enabled": true + }, + { + "key": "roleSearchRequestId-3", + "value": "", + "enabled": true + }, + { + "key": "roleSearchRequestId1-1", + "value": "", + "enabled": true + }, + { + "key": "roleSearchRequestId1-2", + "value": "", + "enabled": true + }, + { + "key": "roleSearchRequestId1-3", + "value": "", + "enabled": true + }, + { + "key": "roleSearchRequestId2-1", + "value": "", + "enabled": true + }, + { + "key": "roleSearchRequestId2-2", + "value": "", + "enabled": true + }, + { + "key": "roleSearchRequestId2-3", + "value": "", + "enabled": true + }, + { + "key": "roleSearchRequestId3-1", + "value": "", + "enabled": true + }, + { + "key": "roleSearchRequestId3-2", + "value": "", + "enabled": true + }, + { + "key": "roleSearchRequestId3-3", + "value": "", + "enabled": true + }, + { + "key": "createTeamProjectId-1", + "value": "", + "enabled": true + }, + { + "key": "createTeamProjectId-2", + "value": "", + "enabled": true + }, + { + "key": "createTeamProjectId-3", + "value": "", + "enabled": true } ], "_postman_variable_scope": "environment", - "_postman_exported_at": "2021-05-27T01:32:45.726Z", + "_postman_exported_at": "2021-06-05T19:51:04.990Z", "_postman_exported_using": "Postman/8.5.1" } \ No newline at end of file diff --git a/migrations/2021-06-04-role-search-request-table-create.js b/migrations/2021-06-04-role-search-request-table-create.js new file mode 100644 index 00000000..2e32ff1a --- /dev/null +++ b/migrations/2021-06-04-role-search-request-table-create.js @@ -0,0 +1,77 @@ +const config = require('config') + +/* + * Create role table + */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('role_search_requests', { + id: { + type: Sequelize.UUID, + primaryKey: true, + allowNull: false, + defaultValue: Sequelize.UUIDV4 + }, + memberId: { + field: 'member_id', + type: Sequelize.UUID + }, + previousRoleSearchRequestId: { + field: 'previous_role_search_request_id', + type: Sequelize.UUID + }, + roleId: { + field: 'role_id', + type: Sequelize.UUID, + references: { + model: { + tableName: 'roles', + schema: config.DB_SCHEMA_NAME + }, + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + jobDescription: { + field: 'job_description', + type: Sequelize.STRING() + }, + skills: { + type: Sequelize.ARRAY({ + type: Sequelize.UUID + }) + }, + createdBy: { + field: 'created_by', + type: Sequelize.UUID, + allowNull: false + }, + updatedBy: { + field: 'updated_by', + type: Sequelize.UUID + }, + createdAt: { + field: 'created_at', + type: Sequelize.DATE + }, + updatedAt: { + field: 'updated_at', + type: Sequelize.DATE + }, + deletedAt: { + field: 'deleted_at', + type: Sequelize.DATE + } + }, { + schema: config.DB_SCHEMA_NAME + }) + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable({ + tableName: 'role_search_requests', + schema: config.DB_SCHEMA_NAME + }) + } +} diff --git a/src/common/helper.js b/src/common/helper.js index 2f01161a..e35c661e 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1791,15 +1791,14 @@ async function getTags (description) { } /** - * @param {Object} currentUser the user performing the action * @param {Object} data title of project and any other info * @returns {Object} the project created */ -async function createProject (currentUser, data) { - const token = currentUser.jwtToken +async function createProject (data) { + const token = await getM2MToken() const res = await request .post(`${config.TC_API}/projects/`) - .set('Authorization', token) + .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') .send(data) @@ -1810,6 +1809,27 @@ async function createProject (currentUser, data) { return _.get(res, 'body') } +/** + * Returns the email address of specified (via handle) user. + * + * @param {String} userHandle user handle + * @returns {String} email address of the user + */ +async function getMemberGroups (userId) { + const token = await getM2MToken() + const url = `${config.TC_API}/groups/memberGroups/${userId}` + const res = await request + .get(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + localLogger.debug({ + context: 'getMemberGroups', + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res, 'body') +} + module.exports = { getParamFromCliArgs, promptUser, @@ -1864,5 +1884,6 @@ module.exports = { extractWorkPeriods, getUserByHandle, substituteStringByObject, - createProject + createProject, + getMemberGroups } diff --git a/src/controllers/SkillController.js b/src/controllers/SkillController.js deleted file mode 100644 index 0dd13ea7..00000000 --- a/src/controllers/SkillController.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Controller for skills endpoints - */ -const service = require('../services/SkillService') -const helper = require('../common/helper') - -/** - * Search skills - * @param req the request - * @param res the response - */ -async function searchSkills (req, res) { - const result = await service.searchSkills(req.query) - helper.setResHeaders(req, res, result) - res.send(result.result) -} - -module.exports = { - searchSkills -} diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index 6afa865d..d7992e14 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -108,14 +108,13 @@ async function getMe (req, res) { res.send(await service.getMe(req.authUser)) } - /** * Return skills by job description. * @param req the request * @param res the response */ -async function getSkillsByJobDescription(req, res) { - res.send(await service.getSkillsByJobDescription(req.authUser, req.body)); +async function getSkillsByJobDescription (req, res) { + res.send(await service.getSkillsByJobDescription(req.authUser, req.body)) } /** @@ -123,8 +122,28 @@ async function getSkillsByJobDescription(req, res) { * @param req the request * @param res the response */ -async function createProj (req, res) { - res.send(await service.createProj(req.authUser, req.body)) +async function roleSearchRequest (req, res) { + res.send(await service.roleSearchRequest(req.authUser, req.body)) +} + +/** + * + * @param req the request + * @param res the response + */ +async function createTeam (req, res) { + res.send(await service.createTeam(req.authUser, req.body)) +} + +/** + * Search skills + * @param req the request + * @param res the response + */ +async function searchSkills (req, res) { + const result = await service.searchSkills(req.query) + helper.setResHeaders(req, res, result) + res.send(result.result) } module.exports = { @@ -138,5 +157,7 @@ module.exports = { deleteMember, getMe, getSkillsByJobDescription, - createProj, -}; + roleSearchRequest, + createTeam, + searchSkills +} diff --git a/src/models/Role.js b/src/models/Role.js index 57cd5025..ab8b4670 100644 --- a/src/models/Role.js +++ b/src/models/Role.js @@ -4,6 +4,15 @@ const errors = require('../common/errors') module.exports = (sequelize) => { class Role extends Model { + /** + * Create association between models + * @param {Object} models the database models + */ + static associate (models) { + Role._models = models + Role.hasMany(models.RoleSearchRequest, { foreignKey: 'roleId' }) + } + /** * Get role by id * @param {String} id the role id diff --git a/src/models/RoleSearchRequest.js b/src/models/RoleSearchRequest.js new file mode 100644 index 00000000..384b74d0 --- /dev/null +++ b/src/models/RoleSearchRequest.js @@ -0,0 +1,110 @@ +const { Sequelize, Model } = require('sequelize') +const config = require('config') +const errors = require('../common/errors') + +module.exports = (sequelize) => { + class RoleSearchRequest extends Model { + /** + * Create association between models + * @param {Object} models the database models + */ + static associate (models) { + RoleSearchRequest._models = models + RoleSearchRequest.belongsTo(models.Role, { + foreignKey: 'roleId' + }) + } + + /** + * Get RoleSearchRequest by id + * @param {String} id the RoleSearchRequest id + * @returns {RoleSearchRequest} the RoleSearchRequest instance + */ + static async findById (id) { + const roleSearchRequest = await RoleSearchRequest.findOne({ + where: { + id + } + }) + if (!roleSearchRequest) { + throw new errors.NotFoundError(`id: ${id} "RoleSearchRequest" doesn't exists.`) + } + return roleSearchRequest + } + } + RoleSearchRequest.init( + { + id: { + type: Sequelize.UUID, + primaryKey: true, + allowNull: false, + defaultValue: Sequelize.UUIDV4 + }, + memberId: { + field: 'member_id', + type: Sequelize.UUID + }, + previousRoleSearchRequestId: { + field: 'previous_role_search_request_id', + type: Sequelize.UUID + }, + roleId: { + field: 'role_id', + type: Sequelize.UUID, + allowNull: true + }, + jobDescription: { + field: 'job_description', + type: Sequelize.STRING() + }, + skills: { + type: Sequelize.ARRAY({ + type: Sequelize.UUID + }) + }, + createdBy: { + field: 'created_by', + type: Sequelize.UUID, + allowNull: false + }, + updatedBy: { + field: 'updated_by', + type: Sequelize.UUID + }, + createdAt: { + field: 'created_at', + type: Sequelize.DATE + }, + updatedAt: { + field: 'updated_at', + type: Sequelize.DATE + }, + deletedAt: { + field: 'deleted_at', + type: Sequelize.DATE + } + }, + { + schema: config.DB_SCHEMA_NAME, + sequelize, + tableName: 'role_search_requests', + paranoid: true, + deletedAt: 'deletedAt', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + timestamps: true, + defaultScope: { + attributes: { + exclude: ['deletedAt'] + } + }, + hooks: { + afterCreate: (role) => { + delete role.dataValues.deletedAt + } + } + } + ) + + return RoleSearchRequest +} diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index 21389c43..b82f4f02 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -22,7 +22,7 @@ module.exports = { }, '/taas-teams/skills': { get: { - controller: 'SkillController', + controller: 'TeamController', method: 'searchSkills', auth: 'jwt', scopes: [constants.Scopes.READ_TAAS_TEAM] @@ -41,8 +41,8 @@ module.exports = { controller: 'TeamController', method: 'getSkillsByJobDescription', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id': { get: { @@ -90,12 +90,20 @@ module.exports = { scopes: [constants.Scopes.READ_TAAS_TEAM] } }, - '/taas-teams/createTeamRequest': { + '/taas-teams/sendRoleSearchRequest': { post: { controller: 'TeamController', - method: 'createProj', + method: 'roleSearchRequest', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] + scopes: [constants.Scopes.CREATE_ROLE_SEARCH_REQUEST] + } + }, + '/taas-teams/submitTeamRequest': { + post: { + controller: 'TeamController', + method: 'createTeam', + auth: 'jwt', + scopes: [constants.Scopes.CREATE_TAAS_TEAM] } } } diff --git a/src/services/SkillService.js b/src/services/SkillService.js deleted file mode 100644 index b3d33025..00000000 --- a/src/services/SkillService.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * This service provides operations of Skill. - */ -const Joi = require('joi') -const helper = require('../common/helper') - -/** - * Search skills - * @param {Object} criteria the search criteria - * @returns {Object} the search result, contain total/page/perPage and result array - */ -async function searchSkills (criteria) { - return helper.getTopcoderSkills(criteria) -} - -searchSkills.schema = Joi.object().keys({ - criteria: Joi.object().keys({ - page: Joi.page(), - perPage: Joi.perPage(), - orderBy: Joi.string() - }).required() -}).required() - -module.exports = { - searchSkills -} diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 531f8257..c32b2776 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -12,6 +12,11 @@ const logger = require('../common/logger') const errors = require('../common/errors') const JobService = require('./JobService') const ResourceBookingService = require('./ResourceBookingService') +const HttpStatus = require('http-status-codes') +const { Op } = require('sequelize') +const models = require('../models') +const Role = models.Role +const RoleSearchRequest = models.RoleSearchRequest const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { return { @@ -669,45 +674,371 @@ getMe.schema = Joi.object() }) .required() +/** + * Searches roles either by roleId or jobDescription or skills + * @param {Object} currentUser the user performing the operation. + * @param {Object} data search request data + * @returns {Object} the created project + */ +async function roleSearchRequest (currentUser, data) { + let role + // if roleId is provided then find role with given id. + if (!_.isUndefined(data.roleId)) { + role = await Role.findById(data.roleId) + role = role.toJSON() + // if skills is provided then use skills to find role + } else if (!_.isUndefined(data.skills)) { + // validate given skillIds and convert them into skill names + const skills = await getSkillNamesByIds(data.skills) + // find the best matching role + role = await getRoleBySkills(skills) + } else { + // if only job description is provided, collect skill names from description + const tags = await getSkillsByJobDescription(currentUser, { description: data.jobDescription }) + // collected tags from description has inconsistency with topcoder skills + // we need to filter invalid skills + const skills = await getSkillNamesByNames(_.map(tags, 'tag')) + // find the best matching role + role = await getRoleBySkills(skills) + } + data.roleId = role.id + // create roleSearchRequest entity with found roleId + const { id: roleSearchRequestId } = await createRoleSearchRequest(currentUser, data) + // clean Role + role = await _cleanRoleDTO(currentUser, role) + // return Role + return _.assign(role, { roleSearchRequestId }) +} + +roleSearchRequest.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + data: Joi.object().keys({ + roleId: Joi.string().uuid(), + jobDescription: Joi.string().max(255), + skills: Joi.array().items(Joi.string().uuid().required()) + }).required().min(1) + }).required() + +/** + * Returns 1 role most relevant to the specified skills + * @param {Array} skills the array of skill names + * @returns {Role} the best matching Role + */ +async function getRoleBySkills (skills) { + // find all roles which includes any of the given skills + const queryCriteria = { + where: { listOfSkills: { [Op.overlap]: skills } }, + raw: true + } + const roles = await Role.findAll(queryCriteria) + if (roles.length > 0) { + let result = _.each(roles, role => { + // calculate each found roles matching rate + role.matchingRate = _.intersection(role.listOfSkills, skills).length / skills.length + // each role can have multiple rates, get the maximum of global rates + role.maxGlobal = _.maxBy(role.rates, 'global').global + }) + // sort roles by matchingRate, global rate and name + result = _.orderBy(result, ['matchingRate', 'maxGlobal', 'name'], ['desc', 'desc', 'asc']) + if (result[0].matchingRate >= config.ROLE_MATCHING_RATE) { + // return the 1st role + return _.omit(result[0], ['matchingRate', 'maxGlobal']) + } + } + // if no matching role found then return Niche role or empty object + return await Role.findOne({ where: { name: { [Op.iLike]: 'Niche' } } }) || {} +} + +getRoleBySkills.schema = Joi.object() + .keys({ + skills: Joi.array().items(Joi.string().required()).required() + }).required() + /** * Return skills by job description. * * @param {Object} currentUser the user who perform this operation. - * @params {Object} criteria the search criteria - * @returns {Object} the user data for current user + * @param {Object} data the search criteria + * @returns {Object} the result */ -async function getSkillsByJobDescription(currentUser,data) { +async function getSkillsByJobDescription (currentUser, data) { return helper.getTags(data.description) } getSkillsByJobDescription.schema = Joi.object() .keys({ currentUser: Joi.object().required(), - data: Joi.object() - .keys({ - description: Joi.string().required(), - }) - .required(), - }) - .required(); + data: Joi.object().keys({ + description: Joi.string().required() + }).required() + }).required() + +/** + * Validate given skillIds and return their names + * + * @param {Array} skills the array of skill ids + * @returns {Array} the array of skill names + */ +async function getSkillNamesByIds (skills) { + const responses = await Promise.all( + skills.map( + skill => helper.getSkillById(skill) + .then((skill) => { + return _.assign(skill, { found: true }) + }) + .catch(err => { + if (err.status !== HttpStatus.NOT_FOUND) { + throw err + } + return { found: false, skill } + }) + ) + ) + const errResponses = responses.filter(res => !res.found) + if (errResponses.length) { + throw new errors.BadRequestError(`Invalid skills: [${errResponses.map(res => res.skill)}]`) + } + return _.map(responses, 'name') +} + +getSkillNamesByIds.schema = Joi.object() + .keys({ + skills: Joi.array().items(Joi.string().uuid().required()).required() + }).required() + +/** + * Finds and returns the ids of given skill names + * + * @param {Array} skills the array of skill names + * @returns {Array} the array of skill ids + */ +async function getSkillIdsByNames (skills) { + const result = await helper.getAllTopcoderSkills({ name: _.join(skills, ',') }) + // endpoint returns the partial matched skills + // we need to filter by exact match case insensitive + const filteredSkills = _.filter(result, tcSkill => _.some(skills, skill => _.toLower(skill) === _.toLower(tcSkill.name))) + console.log(filteredSkills) + const skillIds = _.map(filteredSkills, 'id') + return skillIds +} + +getSkillIdsByNames.schema = Joi.object() + .keys({ + skills: Joi.array().items(Joi.string().required()).required() + }).required() + +/** + * Filters invalid skills from given skill names + * + * @param {Array} skills the array of skill names + * @returns {Array} the array of skill names + */ +async function getSkillNamesByNames (skills) { + // remove duplicates, leading and trailing whitespaces, empties. + const cleanedSkills = _.uniq(_.filter(_.map(skills, skill => _.trim(skill)), skill => !_.isEmpty(skill))) + const result = await helper.getAllTopcoderSkills({ name: _.join(cleanedSkills, ',') }) + const skillNames = _.map(result, 'name') + // endpoint returns the partial matched skills + // we need to filter by exact match case insensitive + return _.intersectionBy(skillNames, cleanedSkills, _.toLower) +} +getSkillNamesByNames.schema = Joi.object() + .keys({ + skills: Joi.array().items(Joi.string().required()).required() + }).required() + +/** + * Creates the role search request + * + * @param {Object} currentUser the user who perform this operation. + * @param {Object} roleSearchRequest the role search request + * @returns {RoleSearchRequest} the role search request entity + */ + +async function createRoleSearchRequest (currentUser, roleSearchRequest) { + roleSearchRequest.createdBy = await helper.getUserId(currentUser.userId) + // if current user is not machine then it must be logged user + if (!currentUser.isMachine) { + roleSearchRequest.memberId = roleSearchRequest.createdBy + // find the previous search done by this user + const previous = await RoleSearchRequest.findOne({ + where: { + memberId: roleSearchRequest.memberId + }, + order: [['createdAt', 'DESC']] + }) + if (previous) { + roleSearchRequest.previousRoleSearchRequestId = previous.id + } + } + const created = await RoleSearchRequest.create(roleSearchRequest) + return created.toJSON() +} +createRoleSearchRequest.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + roleSearchRequest: Joi.object().keys({ + roleId: Joi.string().uuid(), + jobDescription: Joi.string().max(255), + skills: Joi.array().items(Joi.string().uuid().required()) + }).required().min(1) + }).required() + +/** + * Exclude some fields from role if the user is external member + * + * @param {Object} currentUser the user who perform this operation. + * @param {Object} role the role object to be cleaned + * @returns {Object} the cleaned role + */ +async function _cleanRoleDTO (currentUser, role) { + // if current user is machine, it means user is not logged in + if (currentUser.isMachine || await isExternalMember(currentUser.userId)) { + role.isExternalMember = true + if (role.rates) { + role.rates = _.map(role.rates, rate => + _.omit(rate, ['inCountry', 'offShore', 'rate30InCountry', 'rate30OffShore', 'rate20InCountry', 'rate20OffShore'])) + } + return role + } + role.isExternalMember = false + return role +} + +/** + * Finds out if member is external member + * + * @param {number} memberId the external id of member + * @returns {boolean} + */ +async function isExternalMember (memberId) { + const groups = await helper.getMemberGroups(memberId) + return _.intersection(config.INTERNAL_MEMBER_GROUPS, groups).length === 0 +} + +isExternalMember.schema = Joi.object() + .keys({ + memberId: Joi.number().required() + }).required() /** * @param {Object} currentUser the user performing the operation. - * @param {Object} data project data - * @returns {Object} the created project + * @param {Object} data the team data + * @returns {Object} the created project id */ -async function createProj (currentUser, data) { - return helper.createProject(currentUser, data) +async function createTeam (currentUser, data) { + // before creating a project, we should validate the given roleSearchRequestIds + // because if some data is missing it would fail to create jobs. + const roleSearchRequests = await _validateRoleSearchRequests(_.map(data.positions, 'roleSearchRequestId')) + const projectRequestBody = { + name: data.teamName, + description: data.teamDescription, + type: 'app_dev', + details: { + positions: data.positions + } + } + // create project with given data + const project = await helper.createProject(projectRequestBody) + // we created the project with m2m token + // so we have to add the current user as a member to the project + // the role of the user in the project will be determined by user's current roles. + if (!currentUser.isMachine) { + await helper.createProjectMember(project.id, { userId: currentUser.userId }) + } + // create jobs for the given positions. + await Promise.all(_.map(data.positions, async position => { + const roleSearchRequest = roleSearchRequests[position.roleSearchRequestId] + const job = { + projectId: project.id, + title: position.roleName, + numPositions: position.numberOfResources, + rateType: 'weekly', + skills: roleSearchRequest.skills + } + if (roleSearchRequest.jobDescription) { + job.description = roleSearchRequest.jobDescription + } + if (position.startMonth) { + job.startDate = position.startMonth + } + if (position.durationWeeks) { + job.duration = position.durationWeeks + } + if (roleSearchRequest.roleId) { + job.roleIds = [roleSearchRequest.roleId] + } + await JobService.createJob(currentUser, job) + })) + return { projectId: project.id } } -createProj.schema = Joi.object() +createTeam.schema = Joi.object() .keys({ currentUser: Joi.object().required(), - data: Joi.object().required() + data: Joi.object().keys({ + teamName: Joi.string().required(), + teamDescription: Joi.string(), + positions: Joi.array().items( + Joi.object().keys({ + roleName: Joi.string().required(), + roleSearchRequestId: Joi.string().uuid().required(), + numberOfResources: Joi.number().integer().min(1).required(), + durationWeeks: Joi.number().integer().min(1), + startMonth: Joi.date() + }).required() + ).required() + }).required() }) .required() +/** + * @param {Array} roleSearchRequestIds the roleSearchRequestIds + * @returns {Object} the roleSearchRequests + */ +async function _validateRoleSearchRequests (roleSearchRequestIds) { + const roleSearchRequests = {} + await Promise.all(_.map(roleSearchRequestIds, async roleSearchRequestId => { + // check if roleSearchRequest exists + const roleSearchRequest = await RoleSearchRequest.findById(roleSearchRequestId) + // store the found roleSearchRequest to avoid unnecessary DB calls + roleSearchRequests[roleSearchRequestId] = roleSearchRequest.toJSON() + // we can't create a job without skills + if (!roleSearchRequest.roleId && !roleSearchRequest.skills) { + throw new errors.ConflictError(`roleSearchRequestId: ${roleSearchRequestId} must have roleId or skills`) + } + // if roleSearchRequest doesn't have skills, we have to get skills through role + if (!roleSearchRequest.skills) { + const role = await Role.findById(roleSearchRequest.roleId) + if (!role.listOfSkills) { + throw new errors.ConflictError(`role: ${role.id} must have skills`) + } + // store the found skills + roleSearchRequests[roleSearchRequestId].skills = await getSkillIdsByNames(role.listOfSkills) + } + })) + return roleSearchRequests +} + +/** + * Search skills + * @param {Object} criteria the search criteria + * @returns {Object} the search result, contain total/page/perPage and result array + */ +async function searchSkills (criteria) { + return helper.getTopcoderSkills(criteria) +} + +searchSkills.schema = Joi.object().keys({ + criteria: Joi.object().keys({ + page: Joi.page(), + perPage: Joi.perPage(), + orderBy: Joi.string() + }).required() +}).required() + module.exports = { searchTeams, getTeam, @@ -718,6 +1049,14 @@ module.exports = { searchInvites, deleteMember, getMe, + roleSearchRequest, + getRoleBySkills, getSkillsByJobDescription, - createProj, -}; + getSkillNamesByIds, + getSkillIdsByNames, + getSkillNamesByNames, + createRoleSearchRequest, + isExternalMember, + createTeam, + searchSkills +}