From f3146572f47e65f84d84d025318a82bde1ea631e Mon Sep 17 00:00:00 2001 From: eisbilir Date: Mon, 10 May 2021 21:59:57 +0300 Subject: [PATCH 01/86] Get Resource Bookings together with Work Periods --- README.md | 1 - config/default.js | 2 - data/demo-data.json | 4432 ++++++++--------- ...coder-bookings-api.postman_collection.json | 1514 +++++- docs/swagger.yaml | 102 +- package.json | 3 +- scripts/data/exportData.js | 16 +- scripts/data/importData.js | 16 +- scripts/es/createIndex.js | 3 +- scripts/es/deleteIndex.js | 3 +- scripts/es/reIndexAll.js | 17 +- scripts/es/reIndexResourceBookings.js | 17 +- scripts/es/reIndexWorkPeriods.js | 46 - src/common/helper.js | 75 +- src/controllers/ResourceBookingController.js | 2 +- .../ResourceBookingEventHandler.js | 2 +- src/models/ResourceBooking.js | 34 +- src/models/WorkPeriod.js | 5 +- src/services/ResourceBookingService.js | 303 +- src/services/TeamService.js | 2 +- src/services/WorkPeriodPaymentService.js | 47 +- src/services/WorkPeriodService.js | 162 +- test/prepare.js | 5 +- test/unit/ResourceBookingService.test.js | 222 + test/unit/common/CommonData.js | 15 +- test/unit/common/ResourceBookingData.js | 386 +- 26 files changed, 4597 insertions(+), 2835 deletions(-) delete mode 100644 scripts/es/reIndexWorkPeriods.js diff --git a/README.md b/README.md index a7698cde..a53eb10e 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,6 @@ | `npm run index:jobs ` | Indexes job data from db into ES, if jobId is not given all data is indexed. Use `-- --force` flag to skip confirmation | | `npm run index:job-candidates ` | Indexes job candidate data from db into ES, if jobCandidateId is not given all data is indexed. Use `-- --force` flag to skip confirmation | | `npm run index:resource-bookings ` | Indexes resource bookings data from db into ES, if resourceBookingsId is not given all data is indexed. Use `-- --force` flag to skip confirmation | -| `npm run index:work-periods ` | Indexes work periods data from db into ES, if workPeriodId is not given all data is indexed. Use `-- --force` flag to skip confirmation | | `npm run services:up` | Start services via docker-compose for local development. | | `npm run services:down` | Stop services via docker-compose for local development. | | `npm run services:logs -- -f ` | View logs of some service inside docker-compose. | diff --git a/config/default.js b/config/default.js index f7eddb93..2b5ca7ba 100644 --- a/config/default.js +++ b/config/default.js @@ -76,8 +76,6 @@ module.exports = { ES_INDEX_JOB_CANDIDATE: process.env.ES_INDEX_JOB_CANDIDATE || 'job_candidate', // the resource booking index ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking', - // the work period index - ES_INDEX_WORK_PERIOD: process.env.ES_INDEX_WORK_PERIOD || 'work_period', // the max bulk size in MB for ES indexing MAX_BULK_REQUEST_SIZE_MB: process.env.MAX_BULK_REQUEST_SIZE_MB || 20, diff --git a/data/demo-data.json b/data/demo-data.json index 5736c839..357b00c4 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -1,2386 +1,2056 @@ { - "Job": [ - { - "id": "b9887564-3d3d-4c70-8a7b-552576ef2e8d", - "projectId": 111, - "externalId": "0", - "description": "taas-demo-job1", - "title": "Demo Title", - "startDate": "2020-09-27T04:17:23.131Z", - "duration": null, - "numPositions": 13, - "resourceType": "Dummy Resource Type", - "rateType": "weekly", - "workload": "full-time", - "skills": [ - "ee4c50c1-c8c3-475e-b6b6-edbd136a19d6", - "89139c80-d0a2-47c2-aa16-14589d5afd10", - "9f2d9127-6a2e-4506-ad76-c4ab63577b09", - "9515e7ee-83b6-49d1-ba5c-6c59c5a8ef1b", - "c854ab55-5922-4be1-8ecc-b3bc1f8629af", - "8456002e-fa2d-44f0-b0e7-86b1c02b6e4c", - "114b4ec8-805e-4c60-b351-14a955a991a9", - "213408aa-f16f-46c8-bc57-9e569cee3f11", - "b37a48db-f775-4e4e-b403-8ad1d234cdea", - "99b930b5-1b91-4df1-8b17-d9307107bb51", - "6388a632-c3ad-4525-9a73-66a527c03672", - "23839f38-6f19-4de9-9d28-f020056bca73", - "289e42a3-23e9-49be-88e1-6deb93cd8c31", - "b403f209-63b5-42bc-9b5f-1564416640d8" - ], - "status": "sourcing", - "isApplicationPageActive": false, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-01-28T19:36:33.409Z", - "updatedAt": "2021-03-30T19:11:05.033Z" - }, - { - "id": "a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e", - "projectId": 111, - "externalId": "0", - "description": "taas-demo-job2", - "title": "Dummy title - at most 64 characters", - "startDate": "2020-09-27T04:17:23.131Z", - "duration": null, - "numPositions": 7, - "resourceType": "Dummy Resource Type", - "rateType": "weekly", - "workload": "full-time", - "skills": [ - "213408aa-f16f-46c8-bc57-9e569cee3f11", - "b37a48db-f775-4e4e-b403-8ad1d234cdea", - "99b930b5-1b91-4df1-8b17-d9307107bb51", - "6388a632-c3ad-4525-9a73-66a527c03672", - "23839f38-6f19-4de9-9d28-f020056bca73", - "289e42a3-23e9-49be-88e1-6deb93cd8c31", - "b403f209-63b5-42bc-9b5f-1564416640d8" - ], - "status": "in-review", - "isApplicationPageActive": false, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:36:44.975Z", - "updatedAt": "2021-01-28T19:38:17.463Z" - }, - { - "id": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "projectId": 111, - "externalId": "0", - "description": "taas-demo-job3", - "title": "Dummy title - at most 64 characters", - "startDate": "2020-09-27T04:17:23.131Z", - "duration": null, - "numPositions": 7, - "resourceType": "Dummy Resource Type", - "rateType": "weekly", - "workload": "full-time", - "skills": [], - "status": "assigned", - "isApplicationPageActive": false, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:38:23.739Z", - "updatedAt": "2021-01-28T19:41:10.607Z" - }, - { - "id": "84b73f90-0fef-4507-887a-074578e5ef38", - "projectId": 111, - "externalId": "0", - "description": "taas-demo-job4", - "title": "Dummy title - at most 64 characters", - "startDate": "2020-09-27T04:17:23.131Z", - "duration": null, - "numPositions": 7, - "resourceType": "Dummy Resource Type", - "rateType": "weekly", - "workload": "full-time", - "skills": [ - "8456002e-fa2d-44f0-b0e7-86b1c02b6e4c", - "114b4ec8-805e-4c60-b351-14a955a991a9", - "213408aa-f16f-46c8-bc57-9e569cee3f11", - "b37a48db-f775-4e4e-b403-8ad1d234cdea", - "99b930b5-1b91-4df1-8b17-d9307107bb51", - "6388a632-c3ad-4525-9a73-66a527c03672" - ], - "status": "closed", - "isApplicationPageActive": false, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:41:21.892Z", - "updatedAt": "2021-01-28T19:41:28.849Z" - }, - { - "id": "62399aa0-b088-41fe-9e9b-0c8071f1934f", - "projectId": 111, - "externalId": "0", - "description": "taas-demo-job5", - "title": "Dummy title - at most 64 characters", - "startDate": "2020-09-27T04:17:23.131Z", - "duration": null, - "numPositions": 7, - "resourceType": "Dummy Resource Type", - "rateType": "weekly", - "workload": "full-time", - "skills": [ - "b37a48db-f775-4e4e-b403-8ad1d234cdea", - "99b930b5-1b91-4df1-8b17-d9307107bb51", - "6388a632-c3ad-4525-9a73-66a527c03672" - ], - "status": "cancelled", - "isApplicationPageActive": false, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:41:35.098Z", - "updatedAt": "2021-01-28T19:41:42.124Z" - }, - { - "id": "1324da27-9d7d-47d8-a04e-9fb3f35a67fa", - "projectId": 111, - "externalId": "88774632", - "description": "Dummy Description", - "title": "Dummy title - at most 64 characters", - "startDate": "2020-09-27T04:17:23.131Z", - "duration": 1, - "numPositions": 13, - "resourceType": "Dummy Resource Type", - "rateType": "hourly", - "workload": "full-time", - "skills": [ - "23e00d92-207a-4b5b-b3c9-4c5662644941", - "7d076384-ccf6-4e43-a45d-1b24b1e624aa", - "cbac57a3-7180-4316-8769-73af64893158", - "a2b4bc11-c641-4a19-9eb7-33980378f82e" - ], - "status": "in-review", - "isApplicationPageActive": false, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-04-14T08:46:17.739Z", - "updatedAt": "2021-04-14T08:46:23.311Z" - } - ], - "JobCandidate": [ - { - "id": "debadcd8-64bf-4ab8-9cdb-297479eef6f5", - "jobId": "a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e", - "userId": "4dfc6090-4ba8-4387-b5c4-584fcef982ba", - "status": "open", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-01-28T19:37:05.723Z", - "updatedAt": "2021-03-30T19:11:05.043Z", - "interviews": [] - }, - { - "id": "7ff45b8f-2b71-4510-b760-8dfa62e79504", - "jobId": "a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e", - "userId": "243517dd-77d7-4f70-8951-0bc66da83076", - "status": "open", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-01-28T19:37:11.598Z", - "updatedAt": "2021-03-30T19:11:05.043Z", - "interviews": [] - }, - { - "id": "91d63d5f-01d5-419e-89df-6117ea92f535", - "jobId": "a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e", - "userId": "a2e28bf4-1147-41a6-a39f-e2509306f2a6", - "status": "open", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-01-28T19:37:18.066Z", - "updatedAt": "2021-03-30T19:11:05.043Z", - "interviews": [] - }, - { - "id": "257f98d9-45f7-4e13-a6c2-d7e7b6efc9fe", - "jobId": "a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e", - "userId": "b8649393-d32f-4b7f-a156-12e9776acb0e", - "status": "open", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-01-28T19:37:24.095Z", - "updatedAt": "2021-03-30T19:11:05.043Z", - "interviews": [] - }, - { - "id": "a01852d0-fa08-410c-b97b-67580ce62215", - "jobId": "a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e", - "userId": "a0a3a5ce-1de6-465d-91b2-518feb299851", - "status": "open", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-01-28T19:37:29.734Z", - "updatedAt": "2021-03-30T19:11:05.043Z", - "interviews": [] - }, - { - "id": "2fd7ca69-c8ec-4bf3-a7f3-655fbfe3e7df", - "jobId": "a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e", - "userId": "e6958d77-ffaf-4d24-9cdb-6391506695a4", - "status": "open", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-01-28T19:37:44.728Z", - "updatedAt": "2021-03-30T19:11:05.043Z", - "interviews": [] - }, - { - "id": "f0023058-2996-4bba-8c5f-d09a7023be38", - "jobId": "a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e", - "userId": "626bb327-e738-48e3-8f67-1fa2dc677d3c", - "status": "open", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-01-28T19:37:50.619Z", - "updatedAt": "2021-03-30T19:11:05.043Z", - "interviews": [] - }, - { - "id": "a189b34d-acde-4633-b18b-f7a34d7c5a74", - "jobId": "a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e", - "userId": "b49a0adb-1565-4de1-9189-a763c77f5ed4", - "status": "open", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-01-28T19:37:56.456Z", - "updatedAt": "2021-03-30T19:11:05.043Z", - "interviews": [] - }, - { - "id": "5191a860-4327-4c50-b76b-84beba04519b", - "jobId": "a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e", - "userId": "79ce2a3e-7679-48cf-8ac9-0a8ca4c4b463", - "status": "selected", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:36:51.222Z", - "updatedAt": "2021-01-28T19:38:02.293Z", - "interviews": [] - }, - { - "id": "e6d9635c-b122-4f69-9285-09fb1ab30106", - "jobId": "a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e", - "userId": "98ec2c16-442e-4b61-8ad1-66123ee37d3c", - "status": "rejected - other", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:36:58.774Z", - "updatedAt": "2021-01-28T19:38:13.553Z", - "interviews": [] - }, - { - "id": "f67b155e-0f09-4fdd-89a7-d79c5e46cac6", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "userId": "05e988b7-7d54-4c10-ada1-1a04870a88a8", - "status": "open", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-01-28T19:38:38.332Z", - "updatedAt": "2021-03-30T19:11:05.043Z", - "interviews": [] - }, - { - "id": "8ffd33d3-4a43-4719-aee4-8e46be1d8f1c", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "userId": "a2ffdeed-704d-4cf7-b70a-93fcf61de598", - "status": "open", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-01-28T19:38:43.967Z", - "updatedAt": "2021-03-30T19:11:05.043Z", - "interviews": [] - }, - { - "id": "2b8ba549-8878-43d6-ad5f-6a66e3b9d6c9", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "userId": "4709473d-f060-4102-87f8-4d51ff0b34c1", - "status": "open", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-01-28T19:38:50.106Z", - "updatedAt": "2021-03-30T19:11:05.043Z", - "interviews": [] - }, - { - "id": "ae5a81ec-5d05-43c4-8253-847d91a54711", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "userId": "39c7376e-2d5c-4601-bc47-6b60f505814d", - "status": "open", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-01-28T19:38:55.734Z", - "updatedAt": "2021-03-30T19:11:05.043Z", - "interviews": [] - }, - { - "id": "85d6649e-2682-4904-9480-a77b72fef27d", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "userId": "213d2dd9-1fc3-4eda-ad97-2d56e2a84a1e", - "status": "placed", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-01-28T19:38:30.856Z", - "updatedAt": "2021-01-28T19:40:27.209Z", - "interviews": [] - }, - { - "id": "922dfce3-4e06-4387-9fdb-64f70675e86b", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "userId": "dd5adacb-444d-4992-8b7b-0c349be598db", - "status": "placed", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-01-28T19:39:02.435Z", - "updatedAt": "2021-01-28T19:40:49.349Z", - "interviews": [] - }, - { - "id": "c26c38e2-a47d-405b-abc6-fe62a739561c", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "userId": "6d0509c7-5f12-4d84-9a19-8e80ef7ddd66", - "status": "placed", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-01-28T19:39:08.233Z", - "updatedAt": "2021-01-28T19:40:53.659Z", - "interviews": [] - }, - { - "id": "7bef2b37-e1ee-4638-bfc1-c911787ac955", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "userId": "f65e2104-2987-4136-839d-ee4632f0b2e5", - "status": "placed", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-01-28T19:39:13.469Z", - "updatedAt": "2021-01-28T19:40:57.999Z", - "interviews": [] - }, - { - "id": "e9716139-1f40-4bf1-9f8a-77ae4bcc621e", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "userId": "e5e667ad-0950-43c2-8d1d-6e83ad7d1c7e", - "status": "placed", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-01-28T19:39:19.215Z", - "updatedAt": "2021-01-28T19:41:01.953Z", - "interviews": [] - }, - { - "id": "a1731d01-eac9-4eff-8e5a-8a3c99bc66e0", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "userId": "bef43122-426b-4b2b-acdd-9b5b3bd1c0bf", - "status": "placed", - "externalId": null, - "resume": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-01-28T19:39:24.625Z", - "updatedAt": "2021-01-28T19:41:06.370Z", - "interviews": [] - }, - { - "id": "25787cb2-d876-4883-b533-d5e628d213ce", - "jobId": "1324da27-9d7d-47d8-a04e-9fb3f35a67fa", - "userId": "95e7970f-12b4-43b7-ab35-38c34bf033c7", - "status": "interview", - "externalId": "88774631", - "resume": "http://example.com", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T08:46:23.250Z", - "updatedAt": "2021-04-14T08:46:23.250Z", - "interviews": [ + "Job": [ { - "id": "81f03238-1ce2-4d3d-80c5-5ecd5e7e94a2", - "jobCandidateId": "25787cb2-d876-4883-b533-d5e628d213ce", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "30-min-interview", - "round": 1, - "status": "Scheduling", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T08:46:34.597Z", - "updatedAt": "2021-04-14T08:46:34.597Z" + "id": "36dad9f2-98ed-4d3a-9ea7-2cd3d0f8a51a", + "projectId": 111, + "externalId": "88774632", + "description": "Dummy Description", + "title": "Dummy title - at most 64 characters", + "startDate": "2020-09-27T00:00:00.000Z", + "duration": 1, + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "in-review", + "isApplicationPageActive": false, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:21:10.394Z", + "updatedAt": "2021-05-09T21:21:14.010Z" }, { - "id": "75363f1d-46c3-4261-9c21-70019f90a61a", - "jobCandidateId": "25787cb2-d876-4883-b533-d5e628d213ce", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "30-min-interview", - "round": 2, - "status": "Scheduling", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T08:50:15.109Z", - "updatedAt": "2021-04-14T08:50:15.109Z" + "id": "728ff056-63f6-4730-8a9f-3074acad8479", + "projectId": 111, + "externalId": "1212", + "description": "Dummy Description", + "title": "Dummy title - at most 64 characters", + "startDate": "2020-09-27T04:17:23.131Z", + "duration": 1, + "numPositions": 10, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "in-review", + "isApplicationPageActive": false, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:11:26.934Z", + "updatedAt": "2021-05-09T21:14:05.495Z" + }, + { + "id": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "projectId": 111, + "externalId": "1212", + "description": "Dummy Description", + "title": "Dummy title - at most 64 characters", + "startDate": "2020-09-27T04:17:23.131Z", + "duration": 1, + "numPositions": 7, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "in-review", + "isApplicationPageActive": false, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:18.595Z", + "updatedAt": "2021-05-09T21:23:23.474Z" + }, + { + "id": "b28f4e67-324f-4ada-a23a-c27499053ed4", + "projectId": 111, + "externalId": "1212", + "description": "Dummy Description", + "title": "Dummy title - at most 64 characters", + "startDate": "2020-09-27T04:17:23.131Z", + "duration": 1, + "numPositions": 5, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "in-review", + "isApplicationPageActive": false, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:12:09.293Z", + "updatedAt": "2021-05-09T21:14:59.157Z" + } + ], + "JobCandidate": [ + { + "id": "b0fc417b-3f41-4c06-9f2b-8e680c3a03c6", + "jobId": "728ff056-63f6-4730-8a9f-3074acad8479", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:14:05.412Z", + "updatedAt": "2021-05-09T21:14:05.412Z", + "interviews": [] + }, + { + "id": "c637ecf3-8df5-42e7-80d6-daba422e371a", + "jobId": "728ff056-63f6-4730-8a9f-3074acad8479", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:14:41.500Z", + "updatedAt": "2021-05-09T21:14:41.500Z", + "interviews": [] + }, + { + "id": "02a622f4-7894-4ac0-a823-a952ffa1b3f3", + "jobId": "728ff056-63f6-4730-8a9f-3074acad8479", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:14:43.985Z", + "updatedAt": "2021-05-09T21:14:43.985Z", + "interviews": [] + }, + { + "id": "b32b4819-7bfa-49a8-851e-69cdddff8149", + "jobId": "728ff056-63f6-4730-8a9f-3074acad8479", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:14:46.310Z", + "updatedAt": "2021-05-09T21:14:46.310Z", + "interviews": [] + }, + { + "id": "08a67e4d-6857-492c-a3fa-cd7c64e76a69", + "jobId": "728ff056-63f6-4730-8a9f-3074acad8479", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:14:48.449Z", + "updatedAt": "2021-05-09T21:14:48.449Z", + "interviews": [] + }, + { + "id": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", + "jobId": "728ff056-63f6-4730-8a9f-3074acad8479", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:14:50.595Z", + "updatedAt": "2021-05-09T21:14:50.595Z", + "interviews": [ + { + "id": "077aa2ca-5b60-4ad9-a965-1b37e08a5046", + "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", + "googleCalendarId": null, + "customMessage": null, + "xaiTemplate": "interview-30", + "round": 1, + "startTimestamp": null, + "attendeesList": null, + "status": "Completed", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:16:10.887Z", + "updatedAt": "2021-05-09T21:16:10.887Z" + } + ] + }, + { + "id": "827ee401-df04-42e1-abbe-7b97ce7937ff", + "jobId": "728ff056-63f6-4730-8a9f-3074acad8479", + "userId": "95e7970f-12b4-43b7-ab35-38c34bf033c7", + "status": "open", + "externalId": "88774631", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:16:34.914Z", + "updatedAt": "2021-05-09T21:16:34.914Z", + "interviews": [ + { + "id": "b1f7ba76-640f-47e2-9463-59e51b51ec60", + "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", + "googleCalendarId": "dummyId", + "customMessage": "This is a custom message", + "xaiTemplate": "interview-30", + "round": 2, + "startTimestamp": null, + "attendeesList": null, + "status": "Scheduling", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:17:23.517Z", + "updatedAt": "2021-05-09T21:17:23.517Z" + }, + { + "id": "3144fa65-ea1a-4bec-81b0-7cb1c8845826", + "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", + "googleCalendarId": null, + "customMessage": null, + "xaiTemplate": "interview-30", + "round": 1, + "startTimestamp": null, + "attendeesList": null, + "status": "Completed", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:16:39.019Z", + "updatedAt": "2021-05-09T21:16:39.019Z" + } + ] + }, + { + "id": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", + "jobId": "36dad9f2-98ed-4d3a-9ea7-2cd3d0f8a51a", + "userId": "95e7970f-12b4-43b7-ab35-38c34bf033c7", + "status": "open", + "externalId": "88774631", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:21:13.939Z", + "updatedAt": "2021-05-09T21:21:13.939Z", + "interviews": [ + { + "id": "976d23a9-5710-453f-99d9-f57a588bb610", + "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", + "googleCalendarId": "dummyId", + "customMessage": "This is a custom message", + "xaiTemplate": "interview-30", + "round": 3, + "startTimestamp": null, + "attendeesList": [ + "attendee1@yopmail.com", + "attendee2@yopmail.com" + ], + "status": "Scheduling", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:21:28.713Z", + "updatedAt": "2021-05-09T21:21:28.713Z" + }, + { + "id": "a23e1bf2-1084-4cfe-a0d8-d83bc6fec655", + "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", + "googleCalendarId": "dummyId", + "customMessage": "This is a custom message", + "xaiTemplate": "interview-30", + "round": 2, + "startTimestamp": null, + "attendeesList": [ + "attendee1@yopmail.com", + "attendee2@yopmail.com" + ], + "status": "Scheduling", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:21:22.428Z", + "updatedAt": "2021-05-09T21:21:22.428Z" + }, + { + "id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb", + "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", + "googleCalendarId": null, + "customMessage": null, + "xaiTemplate": "interview-30", + "round": 1, + "startTimestamp": null, + "attendeesList": null, + "status": "Completed", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:21:17.346Z", + "updatedAt": "2021-05-09T21:21:17.346Z" + } + ] + }, + { + "id": "a65b7ff1-8d79-4d6e-b29b-360473131273", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:23.420Z", + "updatedAt": "2021-05-09T21:25:46.850Z", + "interviews": [] + }, + { + "id": "0967d234-a11e-4af9-8f6c-b88d21277e15", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:41.691Z", + "updatedAt": "2021-05-09T21:25:46.861Z", + "interviews": [] + }, + { + "id": "9674672a-4bc8-454d-a0e8-cb4f1ce1411c", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:35.819Z", + "updatedAt": "2021-05-09T21:25:46.853Z", + "interviews": [] + }, + { + "id": "bab422c9-cf0f-485b-a734-dbb4e8bea4f0", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:39.914Z", + "updatedAt": "2021-05-09T21:25:46.858Z", + "interviews": [] + }, + { + "id": "292a25c4-a895-4983-a66d-fed44e57244e", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:37.962Z", + "updatedAt": "2021-05-09T21:25:46.855Z", + "interviews": [] + }, + { + "id": "68547eee-c130-42a4-bbec-dad62d7c6852", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:43.611Z", + "updatedAt": "2021-05-09T21:25:46.862Z", + "interviews": [] + }, + { + "id": "0e9996f1-2629-4115-9e23-dc57e23811b2", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:47.468Z", + "updatedAt": "2021-05-09T21:25:46.863Z", + "interviews": [] + }, + { + "id": "2cff52f7-4a2f-48b7-9fad-177745cc759b", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:45.506Z", + "updatedAt": "2021-05-09T21:25:46.864Z", + "interviews": [] + }, + { + "id": "74ed50a4-4082-48af-a2e5-33c3cc2e7f10", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:57.355Z", + "updatedAt": "2021-05-09T21:25:46.866Z", + "interviews": [] + }, + { + "id": "113b3e4d-3aed-4602-b952-3fb28e4bc908", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:49.415Z", + "updatedAt": "2021-05-09T21:25:46.867Z", + "interviews": [] + }, + { + "id": "c3f2bddb-39d1-4558-8052-c556b1436d3a", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:51.500Z", + "updatedAt": "2021-05-09T21:25:46.868Z", + "interviews": [] + }, + { + "id": "26fea152-9f15-437f-876a-a3ef211d60b9", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:53.297Z", + "updatedAt": "2021-05-09T21:25:46.869Z", + "interviews": [] + }, + { + "id": "fa137ab6-daee-4201-8cec-9e70086f9c4a", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:55.300Z", + "updatedAt": "2021-05-09T21:25:46.870Z", + "interviews": [] + }, + { + "id": "a591149a-d16a-4287-a520-d021a5c767b1", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:59.282Z", + "updatedAt": "2021-05-09T21:25:46.872Z", + "interviews": [] + }, + { + "id": "7ccb9a34-553e-427d-9f03-3b7caf0216e9", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:24:01.429Z", + "updatedAt": "2021-05-09T21:25:46.873Z", + "interviews": [] + }, + { + "id": "8845ae56-5291-4ce0-8cf6-9e684b152d03", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:24:03.511Z", + "updatedAt": "2021-05-09T21:25:46.874Z", + "interviews": [] + }, + { + "id": "85dddeda-a4cc-48dc-9a1d-8d96bf52bc5d", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:24:05.605Z", + "updatedAt": "2021-05-09T21:25:46.877Z", + "interviews": [] + }, + { + "id": "06450cf6-aafa-436f-8078-b0ced9f99432", + "jobId": "b28f4e67-324f-4ada-a23a-c27499053ed4", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:15:05.339Z", + "updatedAt": "2021-05-09T21:25:50.517Z", + "interviews": [] + }, + { + "id": "d4b2fe37-e0ed-41db-9c31-561ea2a5fb63", + "jobId": "b28f4e67-324f-4ada-a23a-c27499053ed4", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:15:03.606Z", + "updatedAt": "2021-05-09T21:25:50.519Z", + "interviews": [] + }, + { + "id": "aa72b3df-7c94-4a4a-a6cb-bd14d92aadfc", + "jobId": "b28f4e67-324f-4ada-a23a-c27499053ed4", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:14:59.106Z", + "updatedAt": "2021-05-09T21:25:50.520Z", + "interviews": [] + }, + { + "id": "da1be4fc-21d3-4544-abeb-c3434c172cdf", + "jobId": "b28f4e67-324f-4ada-a23a-c27499053ed4", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:15:02.183Z", + "updatedAt": "2021-05-09T21:25:50.521Z", + "interviews": [] + }, + { + "id": "e07848de-ecee-4eec-8163-4f17e40e756b", + "jobId": "b28f4e67-324f-4ada-a23a-c27499053ed4", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:15:00.676Z", + "updatedAt": "2021-05-09T21:25:50.523Z", + "interviews": [] + }, + { + "id": "458de082-d5ea-495a-8939-e8da3bb00e90", + "jobId": "b28f4e67-324f-4ada-a23a-c27499053ed4", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "placed", + "externalId": "300234321", + "resume": "http://example.com", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:15:06.739Z", + "updatedAt": "2021-05-09T21:25:50.525Z", + "interviews": [] + } + ], + "ResourceBooking": [ + { + "id": "c0a12936-77ef-46fa-8c75-6996339e79f6", + "projectId": 111, + "userId": "05e988b7-7d54-4c10-ada1-1a04870a88a8", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "status": "placed", + "startDate": "2020-09-27", + "endDate": "2020-10-27", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:25:46.728Z", + "updatedAt": "2021-05-09T21:25:46.728Z", + "workPeriods": [ + { + "id": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", + "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", + "userHandle": "sachin-wipro", + "projectId": 111, + "startDate": "2020-09-27", + "endDate": "2020-10-03", + "daysWorked": 4, + "memberRate": 27.06, + "customerRate": 13.13, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:25:47.813Z", + "updatedAt": "2021-05-09T21:45:32.659Z", + "payments": [ + { + "id": "03a0163c-472e-4ea6-b8ad-3dc86d418ecf", + "workPeriodId": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 210.19, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:36.932Z", + "updatedAt": "2021-05-09T21:31:36.932Z" + }, + { + "id": "14b266c6-e76a-4042-b439-74fe3e42c90f", + "workPeriodId": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 417.42, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:38.183Z", + "updatedAt": "2021-05-09T21:31:38.183Z" + }, + { + "id": "1c682ea9-ba63-4fcc-b00c-049d2458d3ac", + "workPeriodId": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 57.79, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:35.726Z", + "updatedAt": "2021-05-09T21:31:35.726Z" + } + ] + }, + { + "id": "e8346d7b-4ada-428d-a768-c2989306f63a", + "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", + "userHandle": "sachin-wipro", + "projectId": 111, + "startDate": "2020-10-18", + "endDate": "2020-10-24", + "daysWorked": 4, + "memberRate": 4.08, + "customerRate": 3.89, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:25:47.834Z", + "updatedAt": "2021-05-09T21:45:37.647Z", + "payments": [ + { + "id": "e8f3d379-f5a0-47f6-b37b-cae24f5909e9", + "workPeriodId": "e8346d7b-4ada-428d-a768-c2989306f63a", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 494.46, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:22.403Z", + "updatedAt": "2021-05-09T21:34:22.403Z" + }, + { + "id": "cc235aee-0911-4869-bb49-911507bb31e7", + "workPeriodId": "e8346d7b-4ada-428d-a768-c2989306f63a", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 494.46, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:26.807Z", + "updatedAt": "2021-05-09T21:34:26.807Z" + } + ] + }, + { + "id": "a3bbd01d-535d-4f02-8524-0d61395b84e9", + "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", + "userHandle": "sachin-wipro", + "projectId": 111, + "startDate": "2020-10-25", + "endDate": "2020-10-31", + "daysWorked": 3, + "memberRate": 15.61, + "customerRate": 9.76, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:25:47.824Z", + "updatedAt": "2021-05-09T21:45:48.727Z", + "payments": [ + { + "id": "7c27346e-a23f-46f3-b4d5-88a323fd437d", + "workPeriodId": "a3bbd01d-535d-4f02-8524-0d61395b84e9", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 210.19, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:46.507Z", + "updatedAt": "2021-05-09T21:33:46.507Z" + } + ] + }, + { + "id": "fa030947-2f2f-4976-82ea-0a22ee96635a", + "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", + "userHandle": "sachin-wipro", + "projectId": 111, + "startDate": "2020-10-11", + "endDate": "2020-10-17", + "daysWorked": 4, + "memberRate": 10.82, + "customerRate": 30.71, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:25:47.815Z", + "updatedAt": "2021-05-09T21:45:41.810Z", + "payments": [ + { + "id": "2a4d1488-d27b-4bc8-af68-1a2b893774de", + "workPeriodId": "fa030947-2f2f-4976-82ea-0a22ee96635a", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 477.97, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:31.474Z", + "updatedAt": "2021-05-09T21:34:31.474Z" + } + ] + }, + { + "id": "61c1e7e3-5e0a-4892-9099-872bc4c11a22", + "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", + "userHandle": "sachin-wipro", + "projectId": 111, + "startDate": "2020-10-04", + "endDate": "2020-10-10", + "daysWorked": 4, + "memberRate": 16.71, + "customerRate": 24.11, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:25:47.841Z", + "updatedAt": "2021-05-09T21:45:27.504Z", + "payments": [ + { + "id": "fcd10a26-3548-4f9b-9e2b-20397d057800", + "workPeriodId": "61c1e7e3-5e0a-4892-9099-872bc4c11a22", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 494.46, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:41.381Z", + "updatedAt": "2021-05-09T21:32:41.381Z" + }, + { + "id": "40e862a0-8772-4587-88b4-23acff8eb2e0", + "workPeriodId": "61c1e7e3-5e0a-4892-9099-872bc4c11a22", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 417.42, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:40.091Z", + "updatedAt": "2021-05-09T21:32:40.091Z" + } + ] + } + ] + }, + { + "id": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "projectId": 111, + "userId": "6d0509c7-5f12-4d84-9a19-8e80ef7ddd66", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "status": "closed", + "startDate": "2021-03-27", + "endDate": "2021-04-27", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:26:16.403Z", + "updatedAt": "2021-05-09T21:26:16.403Z", + "workPeriods": [ + { + "id": "b0758857-0221-47a5-a444-e263e5d9e1cf", + "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "userHandle": "aaaa", + "projectId": 111, + "startDate": "2021-03-21", + "endDate": "2021-03-27", + "daysWorked": 5, + "memberRate": 27.06, + "customerRate": 22.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:17.187Z", + "updatedAt": "2021-05-09T21:46:59.354Z", + "payments": [ + { + "id": "c8be508d-2eb5-4712-8bd7-1b28e870abc2", + "workPeriodId": "b0758857-0221-47a5-a444-e263e5d9e1cf", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 448.51, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:02.733Z", + "updatedAt": "2021-05-09T21:34:02.733Z" + }, + { + "id": "9785ae89-05dc-4bcc-a030-52bd0e681d41", + "workPeriodId": "b0758857-0221-47a5-a444-e263e5d9e1cf", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 168.54, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:01.410Z", + "updatedAt": "2021-05-09T21:34:01.410Z" + } + ] + }, + { + "id": "176db0d0-474f-4590-831a-547d596c01b4", + "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "userHandle": "aaaa", + "projectId": 111, + "startDate": "2021-03-28", + "endDate": "2021-04-03", + "daysWorked": 5, + "memberRate": 6.11, + "customerRate": 14.63, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:17.168Z", + "updatedAt": "2021-05-09T21:47:05.373Z", + "payments": [ + { + "id": "c3b71f96-7680-459b-82b2-f6eb3c3f6c8f", + "workPeriodId": "176db0d0-474f-4590-831a-547d596c01b4", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 119.32, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:18.342Z", + "updatedAt": "2021-05-09T21:31:18.342Z" + }, + { + "id": "3ed31706-0e99-4084-81f4-b126a1a68db6", + "workPeriodId": "176db0d0-474f-4590-831a-547d596c01b4", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 144.16, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:17.193Z", + "updatedAt": "2021-05-09T21:31:17.193Z" + } + ] + }, + { + "id": "5a174833-cb08-49f5-b077-cffb8e60ca01", + "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "userHandle": "aaaa", + "projectId": 111, + "startDate": "2021-04-04", + "endDate": "2021-04-10", + "daysWorked": 5, + "memberRate": 4.91, + "customerRate": 24.11, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:17.165Z", + "updatedAt": "2021-05-09T21:47:12.015Z", + "payments": [ + { + "id": "69445cbf-6d94-49a5-b2aa-65459ec78594", + "workPeriodId": "5a174833-cb08-49f5-b077-cffb8e60ca01", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 417.42, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:33.664Z", + "updatedAt": "2021-05-09T21:32:33.664Z" + }, + { + "id": "663f11df-7832-431a-a46e-ad8c890ae52b", + "workPeriodId": "5a174833-cb08-49f5-b077-cffb8e60ca01", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 55.6, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:32.606Z", + "updatedAt": "2021-05-09T21:32:32.606Z" + } + ] + }, + { + "id": "8c9db4fd-78ad-4e59-acba-462487b74c3a", + "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "userHandle": "aaaa", + "projectId": 111, + "startDate": "2021-04-18", + "endDate": "2021-04-24", + "daysWorked": 2, + "memberRate": 27.46, + "customerRate": 25.57, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:17.200Z", + "updatedAt": "2021-05-09T21:47:25.687Z", + "payments": [ + { + "id": "8eb8fc37-5ab0-4480-8806-3d3c57ab38e1", + "workPeriodId": "8c9db4fd-78ad-4e59-acba-462487b74c3a", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 496.54, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:07.419Z", + "updatedAt": "2021-05-09T21:33:07.419Z" + }, + { + "id": "f65930b7-d61d-4923-bdab-54848661f151", + "workPeriodId": "8c9db4fd-78ad-4e59-acba-462487b74c3a", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 57.79, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:06.108Z", + "updatedAt": "2021-05-09T21:33:06.108Z" + } + ] + }, + { + "id": "18881107-cc17-4087-9b2b-a74f04187f73", + "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "userHandle": "aaaa", + "projectId": 111, + "startDate": "2021-04-11", + "endDate": "2021-04-17", + "daysWorked": 2, + "memberRate": 22.82, + "customerRate": 2.32, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:17.202Z", + "updatedAt": "2021-05-09T21:47:30.586Z", + "payments": [ + { + "id": "c456755e-0432-4656-848b-64f9c5dc8f25", + "workPeriodId": "18881107-cc17-4087-9b2b-a74f04187f73", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 32.92, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:27.102Z", + "updatedAt": "2021-05-09T21:31:27.102Z" + }, + { + "id": "dd8f5c08-d6a1-4fd2-b6bd-85fb6425d13d", + "workPeriodId": "18881107-cc17-4087-9b2b-a74f04187f73", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 372.18, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:25.690Z", + "updatedAt": "2021-05-09T21:31:25.690Z" + } + ] + }, + { + "id": "9b455e21-e186-4622-923a-f115d23549d1", + "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "userHandle": "aaaa", + "projectId": 111, + "startDate": "2021-04-25", + "endDate": "2021-05-01", + "daysWorked": 2, + "memberRate": 22.82, + "customerRate": 10.1, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:17.161Z", + "updatedAt": "2021-05-09T21:47:19.034Z", + "payments": [ + { + "id": "10584b23-5ab2-44e2-a927-e020c08e4f84", + "workPeriodId": "9b455e21-e186-4622-923a-f115d23549d1", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 293.79, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:29.348Z", + "updatedAt": "2021-05-09T21:33:29.348Z" + }, + { + "id": "6d59a499-44e3-41e2-8368-5baee86dd8ab", + "workPeriodId": "9b455e21-e186-4622-923a-f115d23549d1", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 57.79, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:28.108Z", + "updatedAt": "2021-05-09T21:33:28.108Z" + } + ] + } + ] + }, + { + "id": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "projectId": 111, + "userId": "a2ffdeed-704d-4cf7-b70a-93fcf61de598", + "jobId": "b28f4e67-324f-4ada-a23a-c27499053ed4", + "status": "closed", + "startDate": "2021-05-10", + "endDate": "2021-06-15", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": null, + "createdAt": "2021-05-09T21:27:15.093Z", + "updatedAt": "2021-05-09T21:27:15.093Z", + "workPeriods": [ + { + "id": "ac0ae325-8d77-4a73-bd85-5361165801cd", + "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "userHandle": "lakshmiaconnmgr", + "projectId": 111, + "startDate": "2021-06-06", + "endDate": "2021-06-12", + "daysWorked": 3, + "memberRate": 13.74, + "customerRate": 14.57, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:15.951Z", + "updatedAt": "2021-05-09T21:46:12.086Z", + "payments": [ + { + "id": "00640d2d-8330-445a-b022-aa687033b2b3", + "workPeriodId": "ac0ae325-8d77-4a73-bd85-5361165801cd", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 275.73, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:54.315Z", + "updatedAt": "2021-05-09T21:33:54.315Z" + }, + { + "id": "05d09e2b-02a0-4d33-b6db-0f69a98154c6", + "workPeriodId": "ac0ae325-8d77-4a73-bd85-5361165801cd", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 374.34, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:53.131Z", + "updatedAt": "2021-05-09T21:33:53.131Z" + } + ] + }, + { + "id": "9d914249-82f2-422e-9ba6-c281da411266", + "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "userHandle": "lakshmiaconnmgr", + "projectId": 111, + "startDate": "2021-05-16", + "endDate": "2021-05-22", + "daysWorked": 5, + "memberRate": null, + "customerRate": 9.46, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:16.017Z", + "updatedAt": "2021-05-09T21:46:21.053Z", + "payments": [ + { + "id": "6036c7cc-b180-4f50-8163-5fd2541c66b5", + "workPeriodId": "9d914249-82f2-422e-9ba6-c281da411266", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 494.46, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:40.984Z", + "updatedAt": "2021-05-09T21:33:40.984Z" + } + ] + }, + { + "id": "4ae26d58-1910-4ace-9f09-4c62c8df6031", + "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "userHandle": "lakshmiaconnmgr", + "projectId": 111, + "startDate": "2021-06-13", + "endDate": "2021-06-19", + "daysWorked": 3, + "memberRate": 15.61, + "customerRate": 2.1, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:15.972Z", + "updatedAt": "2021-05-09T21:45:57.739Z", + "payments": [ + { + "id": "e623d9c7-caa9-4e58-83f1-44741b1169f8", + "workPeriodId": "4ae26d58-1910-4ace-9f09-4c62c8df6031", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 293.79, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:19.371Z", + "updatedAt": "2021-05-09T21:32:19.371Z" + } + ] + }, + { + "id": "94dde794-b730-4e05-8ea6-dcc5b541d43e", + "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "userHandle": "lakshmiaconnmgr", + "projectId": 111, + "startDate": "2021-05-23", + "endDate": "2021-05-29", + "daysWorked": 3, + "memberRate": 10.82, + "customerRate": 29.89, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:15.991Z", + "updatedAt": "2021-05-09T21:46:02.038Z", + "payments": [ + { + "id": "be9706ac-c6cb-4fff-894b-8719bcf634dc", + "workPeriodId": "94dde794-b730-4e05-8ea6-dcc5b541d43e", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 275.73, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:21.933Z", + "updatedAt": "2021-05-09T21:33:21.933Z" + }, + { + "id": "c7013bf0-17b5-4b15-826b-385fad41caf4", + "workPeriodId": "94dde794-b730-4e05-8ea6-dcc5b541d43e", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 144.16, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:20.680Z", + "updatedAt": "2021-05-09T21:33:20.680Z" + } + ] + }, + { + "id": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", + "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "userHandle": "lakshmiaconnmgr", + "projectId": 111, + "startDate": "2021-05-30", + "endDate": "2021-06-05", + "daysWorked": 3, + "memberRate": 29.32, + "customerRate": 29.89, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:16.013Z", + "updatedAt": "2021-05-09T21:45:53.796Z", + "payments": [ + { + "id": "fc577d14-78e8-404c-a17b-ab496e4041d8", + "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 203.74, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:41.520Z", + "updatedAt": "2021-05-09T21:34:41.520Z" + }, + { + "id": "fbc2d96f-f6c6-4a4d-b737-14a3564b7f70", + "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 448.51, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:38.700Z", + "updatedAt": "2021-05-09T21:34:38.700Z" + }, + { + "id": "10fd3b3e-f5b2-42cc-91d4-54c73c003aae", + "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 477.97, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:37.374Z", + "updatedAt": "2021-05-09T21:34:37.374Z" + }, + { + "id": "6e1a114f-bfac-4ab8-93d6-e47206200540", + "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 494.46, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:40.036Z", + "updatedAt": "2021-05-09T21:34:40.036Z" + } + ] + }, + { + "id": "6a567293-7189-4b14-82c9-57bd3d0eab20", + "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "userHandle": "lakshmiaconnmgr", + "projectId": 111, + "startDate": "2021-05-09", + "endDate": "2021-05-15", + "daysWorked": 3, + "memberRate": 24.03, + "customerRate": 24.11, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:15.959Z", + "updatedAt": "2021-05-09T21:46:07.315Z", + "payments": [ + { + "id": "e386f5a1-2856-4cc5-8304-1598e5683a0f", + "workPeriodId": "6a567293-7189-4b14-82c9-57bd3d0eab20", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 57.79, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:47.852Z", + "updatedAt": "2021-05-09T21:32:47.852Z" + } + ] + } + ] + }, + { + "id": "d6103727-6615-4168-8169-0485577bfb3f", + "projectId": 111, + "userId": "bef43122-426b-4b2b-acdd-9b5b3bd1c0bf", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "status": "placed", + "startDate": "2021-03-27", + "endDate": "2021-04-27", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:27:11.914Z", + "updatedAt": "2021-05-09T21:27:11.914Z", + "workPeriods": [ + { + "id": "d111a56f-593d-452e-9787-551bea504c92", + "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", + "userHandle": "testcat", + "projectId": 111, + "startDate": "2021-04-04", + "endDate": "2021-04-10", + "daysWorked": 4, + "memberRate": 23.2, + "customerRate": 30.71, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:12.731Z", + "updatedAt": "2021-05-09T21:48:08.381Z", + "payments": [ + { + "id": "fa9bd31c-6c83-4ee4-9d45-a833cfe821f5", + "workPeriodId": "d111a56f-593d-452e-9787-551bea504c92", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 271.42, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:11.662Z", + "updatedAt": "2021-05-09T21:34:11.662Z" + }, + { + "id": "abb79afc-a370-4625-a067-a3b57c9b4700", + "workPeriodId": "d111a56f-593d-452e-9787-551bea504c92", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 448.51, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:10.371Z", + "updatedAt": "2021-05-09T21:34:10.371Z" + } + ] + }, + { + "id": "061f31fb-4f8c-462f-92c2-e5d275c45fde", + "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", + "userHandle": "testcat", + "projectId": 111, + "startDate": "2021-03-28", + "endDate": "2021-04-03", + "daysWorked": 2, + "memberRate": 8.09, + "customerRate": 22.15, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:12.733Z", + "updatedAt": "2021-05-09T21:47:38.022Z", + "payments": [ + { + "id": "c658d66e-86e1-49c7-8051-2b9a017935ad", + "workPeriodId": "061f31fb-4f8c-462f-92c2-e5d275c45fde", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 248.38, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:30:41.304Z", + "updatedAt": "2021-05-09T21:30:41.304Z" + }, + { + "id": "3770680c-8045-43d4-8baf-cb7b3b714d39", + "workPeriodId": "061f31fb-4f8c-462f-92c2-e5d275c45fde", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 477.97, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:30:42.711Z", + "updatedAt": "2021-05-09T21:30:42.711Z" + } + ] + }, + { + "id": "5904b1d9-cb50-4b5d-8103-6741fec2f86b", + "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", + "userHandle": "testcat", + "projectId": 111, + "startDate": "2021-04-18", + "endDate": "2021-04-24", + "daysWorked": 2, + "memberRate": 22.82, + "customerRate": 4.07, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:12.746Z", + "updatedAt": "2021-05-09T21:47:44.291Z", + "payments": [ + { + "id": "be4d5099-8b8e-45e2-b6cd-ab1997f57e26", + "workPeriodId": "5904b1d9-cb50-4b5d-8103-6741fec2f86b", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 477.97, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:26.174Z", + "updatedAt": "2021-05-09T21:32:26.174Z" + }, + { + "id": "dfc9bed6-78f2-407e-a7e4-abea9a3d3b46", + "workPeriodId": "5904b1d9-cb50-4b5d-8103-6741fec2f86b", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 48.51, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:27.398Z", + "updatedAt": "2021-05-09T21:32:27.398Z" + } + ] + }, + { + "id": "6ed56eb5-cadc-45f8-bbdf-3f408948c274", + "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", + "userHandle": "testcat", + "projectId": 111, + "startDate": "2021-03-21", + "endDate": "2021-03-27", + "daysWorked": 4, + "memberRate": 22.85, + "customerRate": 29.89, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:12.737Z", + "updatedAt": "2021-05-09T21:47:58.216Z", + "payments": [ + { + "id": "3bf29da2-581c-4f9c-8b0f-ff3c876848a0", + "workPeriodId": "6ed56eb5-cadc-45f8-bbdf-3f408948c274", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 293.79, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:59.500Z", + "updatedAt": "2021-05-09T21:32:59.500Z" + }, + { + "id": "ea3bdc7a-7c14-4ac1-955d-2540589fcfa6", + "workPeriodId": "6ed56eb5-cadc-45f8-bbdf-3f408948c274", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 448.51, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:00.899Z", + "updatedAt": "2021-05-09T21:33:00.899Z" + } + ] + }, + { + "id": "6adbf80c-43aa-41ad-b7db-544dc76f9b1c", + "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", + "userHandle": "testcat", + "projectId": 111, + "startDate": "2021-04-25", + "endDate": "2021-05-01", + "daysWorked": 0, + "memberRate": 29.32, + "customerRate": 22.15, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:12.794Z", + "updatedAt": "2021-05-09T21:47:51.348Z", + "payments": [ + { + "id": "e85008c3-feff-40ab-bf4c-bc80f0bb5288", + "workPeriodId": "6adbf80c-43aa-41ad-b7db-544dc76f9b1c", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 460.88, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:53.856Z", + "updatedAt": "2021-05-09T21:32:53.856Z" + } + ] + }, + { + "id": "0c825dec-6e7b-4dde-943a-f3f8354219cc", + "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", + "userHandle": "testcat", + "projectId": 111, + "startDate": "2021-04-11", + "endDate": "2021-04-17", + "daysWorked": 4, + "memberRate": 5.59, + "customerRate": 30.54, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:12.783Z", + "updatedAt": "2021-05-09T21:48:03.740Z", + "payments": [ + { + "id": "5bdd5c22-d9b7-428c-b084-d0950a18bc37", + "workPeriodId": "0c825dec-6e7b-4dde-943a-f3f8354219cc", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 57.79, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:30:49.123Z", + "updatedAt": "2021-05-09T21:30:49.123Z" + }, + { + "id": "78641310-ad51-40a3-a0fd-fdd8d15455b9", + "workPeriodId": "0c825dec-6e7b-4dde-943a-f3f8354219cc", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 417.42, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:30:50.460Z", + "updatedAt": "2021-05-09T21:30:50.460Z" + } + ] + } + ] + }, + { + "id": "314084fc-e854-4b62-8901-03ea9bbf2ffa", + "projectId": 111, + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "jobId": "b28f4e67-324f-4ada-a23a-c27499053ed4", + "status": "cancelled", + "startDate": "2020-12-27", + "endDate": "2021-01-27", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": null, + "createdAt": "2021-05-09T21:25:50.450Z", + "updatedAt": "2021-05-09T21:25:50.450Z", + "workPeriods": [ + { + "id": "13f22f72-9240-43bb-ba0c-618d0b24ad8c", + "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", + "userHandle": "pshah_manager", + "projectId": 111, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 24.35, + "customerRate": 4.79, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:25:51.398Z", + "updatedAt": "2021-05-09T21:45:01.792Z", + "payments": [ + { + "id": "381af41e-6b0a-49fe-987f-1bcb03fda571", + "workPeriodId": "13f22f72-9240-43bb-ba0c-618d0b24ad8c", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 460.88, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:08.119Z", + "updatedAt": "2021-05-09T21:31:08.119Z" + }, + { + "id": "1d2b92e8-194f-477a-97f8-e104056e6b10", + "workPeriodId": "13f22f72-9240-43bb-ba0c-618d0b24ad8c", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 39.66, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:09.459Z", + "updatedAt": "2021-05-09T21:31:09.459Z" + } + ] + }, + { + "id": "0cf74043-b432-41a5-99d9-83420a6ad8ef", + "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", + "userHandle": "pshah_manager", + "projectId": 111, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 2, + "memberRate": 13.74, + "customerRate": 21.1, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:25:51.419Z", + "updatedAt": "2021-05-09T21:44:51.852Z", + "payments": [ + { + "id": "ef951baa-a007-48db-a658-495c6eeda9bc", + "workPeriodId": "0cf74043-b432-41a5-99d9-83420a6ad8ef", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 448.51, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:30:59.323Z", + "updatedAt": "2021-05-09T21:30:59.323Z" + }, + { + "id": "f0f85e56-6bf4-4e1f-a1ef-c31529efe4cd", + "workPeriodId": "0cf74043-b432-41a5-99d9-83420a6ad8ef", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 466.42, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:30:58.017Z", + "updatedAt": "2021-05-09T21:30:58.017Z" + } + ] + }, + { + "id": "028287bf-6999-4fef-bdfa-1229b4e23ac1", + "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", + "userHandle": "pshah_manager", + "projectId": 111, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 27.06, + "customerRate": 2.54, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:25:51.432Z", + "updatedAt": "2021-05-09T21:45:16.298Z", + "payments": [ + { + "id": "1cb5129e-8d92-4280-a946-8cb0f5757abc", + "workPeriodId": "028287bf-6999-4fef-bdfa-1229b4e23ac1", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 460.88, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:30:33.549Z", + "updatedAt": "2021-05-09T21:30:33.549Z" + }, + { + "id": "b576f845-7dea-4b56-b0de-6ce15fd2c245", + "workPeriodId": "028287bf-6999-4fef-bdfa-1229b4e23ac1", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 477.97, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:30:25.688Z", + "updatedAt": "2021-05-09T21:30:25.688Z" + } + ] + }, + { + "id": "e71f17b2-acb4-4c14-a492-8cab071fe2b5", + "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", + "userHandle": "pshah_manager", + "projectId": 111, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 4, + "memberRate": 15.61, + "customerRate": 2.54, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:25:51.383Z", + "updatedAt": "2021-05-09T21:45:22.801Z", + "payments": [ + { + "id": "a376dfdc-3330-417b-916d-3a7d4f9ab384", + "workPeriodId": "e71f17b2-acb4-4c14-a492-8cab071fe2b5", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 466.42, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:17.363Z", + "updatedAt": "2021-05-09T21:34:17.363Z" + } + ] + }, + { + "id": "32b977c9-386a-4159-a1c3-08169ee12f6e", + "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", + "userHandle": "pshah_manager", + "projectId": 111, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 3, + "memberRate": 13.74, + "customerRate": 22.37, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:25:51.435Z", + "updatedAt": "2021-05-09T21:45:08.968Z", + "payments": [ + { + "id": "fa4c7e24-470f-4aef-a269-59b7e0b2bc05", + "workPeriodId": "32b977c9-386a-4159-a1c3-08169ee12f6e", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 460.88, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:56.152Z", + "updatedAt": "2021-05-09T21:31:56.152Z" + }, + { + "id": "99e4bffb-90f8-411e-9a49-fc7779bd2c07", + "workPeriodId": "32b977c9-386a-4159-a1c3-08169ee12f6e", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 416.38, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:57.470Z", + "updatedAt": "2021-05-09T21:31:57.470Z" + } + ] + } + ] + }, + { + "id": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", + "projectId": 111, + "userId": "dd5adacb-444d-4992-8b7b-0c349be598db", + "jobId": "b28f4e67-324f-4ada-a23a-c27499053ed4", + "status": "placed", + "startDate": "2021-05-10", + "endDate": "2021-06-15", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": null, + "createdAt": "2021-05-09T21:26:30.065Z", + "updatedAt": "2021-05-09T21:26:30.065Z", + "workPeriods": [ + { + "id": "28bd80ec-ef91-4516-90d6-6979e6cc341c", + "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", + "userHandle": "amy_admin", + "projectId": 111, + "startDate": "2021-05-23", + "endDate": "2021-05-29", + "daysWorked": 5, + "memberRate": 28.4, + "customerRate": 12.25, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:30.824Z", + "updatedAt": "2021-05-09T21:46:34.331Z", + "payments": [ + { + "id": "4892cd43-a132-4ca9-a511-8d10cdc22f5d", + "workPeriodId": "28bd80ec-ef91-4516-90d6-6979e6cc341c", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 374.34, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:43.808Z", + "updatedAt": "2021-05-09T21:31:43.808Z" + } + ] + }, + { + "id": "355c7114-753a-4f99-b026-1d1430bf5530", + "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", + "userHandle": "amy_admin", + "projectId": 111, + "startDate": "2021-05-16", + "endDate": "2021-05-22", + "daysWorked": 5, + "memberRate": 9.26, + "customerRate": 16.44, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:30.865Z", + "updatedAt": "2021-05-09T21:46:39.040Z", + "payments": [ + { + "id": "d0ebcc96-70f2-4716-92d4-74e40af04387", + "workPeriodId": "355c7114-753a-4f99-b026-1d1430bf5530", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 416.38, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:04.463Z", + "updatedAt": "2021-05-09T21:32:04.463Z" + }, + { + "id": "71b3b7d4-129c-4348-9ead-6f22eafa6db8", + "workPeriodId": "355c7114-753a-4f99-b026-1d1430bf5530", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 416.38, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:05.827Z", + "updatedAt": "2021-05-09T21:32:05.827Z" + } + ] + }, + { + "id": "9c024b6a-229a-4b8d-ac03-a53e4e412a73", + "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", + "userHandle": "amy_admin", + "projectId": 111, + "startDate": "2021-05-09", + "endDate": "2021-05-15", + "daysWorked": 5, + "memberRate": 5.59, + "customerRate": 18.55, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:30.813Z", + "updatedAt": "2021-05-09T21:46:53.821Z", + "payments": [ + { + "id": "28f285ec-0965-4904-901d-7ae5c5d8e220", + "workPeriodId": "9c024b6a-229a-4b8d-ac03-a53e4e412a73", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 496.54, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:35.441Z", + "updatedAt": "2021-05-09T21:33:35.441Z" + } + ] + }, + { + "id": "48c4b614-6588-46c9-8b4f-54d3175ae47d", + "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", + "userHandle": "amy_admin", + "projectId": 111, + "startDate": "2021-06-06", + "endDate": "2021-06-12", + "daysWorked": 5, + "memberRate": 16.02, + "customerRate": 30.71, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:30.878Z", + "updatedAt": "2021-05-09T21:46:27.840Z", + "payments": [ + { + "id": "e1f9634a-4c3b-4dcd-ad81-3703c807a820", + "workPeriodId": "48c4b614-6588-46c9-8b4f-54d3175ae47d", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 293.79, + "status": "completed", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:12.499Z", + "updatedAt": "2021-05-09T21:32:12.499Z" + } + ] + }, + { + "id": "91fcf91f-b2cf-4909-8f03-b5efc0732b28", + "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", + "userHandle": "amy_admin", + "projectId": 111, + "startDate": "2021-06-13", + "endDate": "2021-06-19", + "daysWorked": 5, + "memberRate": 1.53, + "customerRate": 2.1, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:30.893Z", + "updatedAt": "2021-05-09T21:46:44.896Z", + "payments": [ + { + "id": "d36afd1d-1f76-4f2d-b630-69bf85796496", + "workPeriodId": "91fcf91f-b2cf-4909-8f03-b5efc0732b28", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 494.46, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:12.838Z", + "updatedAt": "2021-05-09T21:33:12.838Z" + }, + { + "id": "71750282-0ffe-46ed-b8c2-37e36c148833", + "workPeriodId": "91fcf91f-b2cf-4909-8f03-b5efc0732b28", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 203.74, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:14.202Z", + "updatedAt": "2021-05-09T21:33:14.202Z" + } + ] + }, + { + "id": "311a692f-18bc-4eb6-a6d0-06cf3dbd1637", + "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", + "userHandle": "amy_admin", + "projectId": 111, + "startDate": "2021-05-30", + "endDate": "2021-06-05", + "daysWorked": 5, + "memberRate": 13.07, + "customerRate": 10.1, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:30.875Z", + "updatedAt": "2021-05-09T21:46:49.092Z", + "payments": [ + { + "id": "875c7250-50fa-453f-99d0-e76260648cd1", + "workPeriodId": "311a692f-18bc-4eb6-a6d0-06cf3dbd1637", + "challengeId": "00000000-0000-0000-0000-000000000000", + "amount": 248.38, + "status": "cancelled", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:50.023Z", + "updatedAt": "2021-05-09T21:31:50.023Z" + } + ] + } + ] } - ] - } - ], - "ResourceBooking": [ - { - "id": "08f5e4b9-1088-496d-91a7-5b22a3583e3c", - "projectId": 111, - "userId": "213d2dd9-1fc3-4eda-ad97-2d56e2a84a1e", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "status": "placed", - "startDate": "2021-01-25", - "endDate": "2021-01-31", - "memberRate": 1000, - "customerRate": 1200, - "rateType": "weekly", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:39:30.052Z", - "updatedAt": "2021-01-28T19:40:25.260Z" - }, - { - "id": "7d967fed-9792-4768-98a7-0b644aa84f2e", - "projectId": 111, - "userId": "05e988b7-7d54-4c10-ada1-1a04870a88a8", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "status": "in-review", - "startDate": "2021-03-25", - "endDate": "2021-05-31", - "memberRate": 1000, - "customerRate": 1200, - "rateType": "weekly", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:39:35.571Z", - "updatedAt": "2021-01-28T19:40:30.291Z" - }, - { - "id": "0a6799d7-f5d1-456b-8bf1-90619284b295", - "projectId": 111, - "userId": "6d0509c7-5f12-4d84-9a19-8e80ef7ddd66", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "status": "placed", - "startDate": "2021-02-27", - "endDate": "2021-03-15", - "memberRate": 2000, - "customerRate": 2500, - "rateType": "weekly", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:40:04.761Z", - "updatedAt": "2021-01-28T19:40:52.303Z" - }, - { - "id": "35e1abd8-1890-4664-bb52-aade382d7b66", - "projectId": 111, - "userId": "a2ffdeed-704d-4cf7-b70a-93fcf61de598", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "status": "closed", - "startDate": "2021-02-25", - "endDate": "2021-04-01", - "memberRate": 1000, - "customerRate": 1200, - "rateType": "weekly", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:39:41.205Z", - "updatedAt": "2021-01-28T19:40:34.859Z" - }, - { - "id": "51b45f5d-5df2-46d5-9c3d-8a1323df38dd", - "projectId": 111, - "userId": "dd5adacb-444d-4992-8b7b-0c349be598db", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "status": "placed", - "startDate": "2021-03-18", - "endDate": "2021-05-28", - "memberRate": 800, - "customerRate": 1000, - "rateType": "weekly", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:39:59.432Z", - "updatedAt": "2021-01-28T19:40:47.743Z" - }, - { - "id": "61f5d474-e41f-490b-ab58-9f983e3d4916", - "projectId": 111, - "userId": "f65e2104-2987-4136-839d-ee4632f0b2e5", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "status": "placed", - "startDate": "2000-03-27", - "endDate": "2000-04-27", - "memberRate": 3000, - "customerRate": 3500, - "rateType": "weekly", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:40:09.879Z", - "updatedAt": "2021-01-28T19:40:56.381Z" - }, - { - "id": "8173579e-4b3c-418d-a9a1-c999caa38404", - "projectId": 111, - "userId": "bef43122-426b-4b2b-acdd-9b5b3bd1c0bf", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "status": "placed", - "startDate": "2020-04-27", - "endDate": "2020-05-27", - "memberRate": 0, - "customerRate": 0, - "rateType": "weekly", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:40:20.627Z", - "updatedAt": "2021-01-28T19:41:04.919Z" - }, - { - "id": "a098e8d8-ce5b-47d9-afee-38b050d16745", - "projectId": 111, - "userId": "4709473d-f060-4102-87f8-4d51ff0b34c1", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "status": "cancelled", - "startDate": "2021-04-25", - "endDate": "2021-04-30", - "memberRate": 1000, - "customerRate": 1200, - "rateType": "weekly", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:39:46.515Z", - "updatedAt": "2021-01-28T19:40:38.820Z" - }, - { - "id": "d38a6223-3f91-4300-9ecb-6e5fee173625", - "projectId": 111, - "userId": "39c7376e-2d5c-4601-bc47-6b60f505814d", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "status": "sourcing", - "startDate": "2021-05-25", - "endDate": "2021-07-31", - "memberRate": 1000, - "customerRate": 1200, - "rateType": "weekly", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:39:52.063Z", - "updatedAt": "2021-01-28T19:40:43.021Z" - }, - { - "id": "dc4477ec-07f8-4c8e-a8fe-ffe38dd290fa", - "projectId": 111, - "userId": "e5e667ad-0950-43c2-8d1d-6e83ad7d1c7e", - "jobId": "2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0", - "status": "placed", - "startDate": "2021-07-27", - "endDate": "2021-09-27", - "memberRate": 1700, - "customerRate": 1900, - "rateType": "weekly", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-28T19:40:15.326Z", - "updatedAt": "2021-01-28T19:41:00.503Z" - } - ], - "WorkPeriod": [ - { - "id": "07783b60-b726-41c2-8955-7766a27c1ec5", - "resourceBookingId": "7d967fed-9792-4768-98a7-0b644aa84f2e", - "userHandle": "sachin-wipro", - "projectId": 111, - "startDate": "2021-03-21", - "endDate": "2021-03-27", - "daysWorked": 2, - "memberRate": 4.8, - "customerRate": 4.95, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.705Z", - "updatedAt": null - }, - { - "id": "6ed667a5-275e-4f27-984b-34dff5f8a1ff", - "resourceBookingId": "dc4477ec-07f8-4c8e-a8fe-ffe38dd290fa", - "userHandle": "nskumar278", - "projectId": 111, - "startDate": "2021-08-01", - "endDate": "2021-08-07", - "daysWorked": 5, - "memberRate": 25.21, - "customerRate": 10.05, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.703Z", - "updatedAt": null - }, - { - "id": "d049e7bc-c827-482b-9ec6-b87d9289d1cd", - "resourceBookingId": "8173579e-4b3c-418d-a9a1-c999caa38404", - "userHandle": "testcat", - "projectId": 111, - "startDate": "2020-05-24", - "endDate": "2020-05-30", - "daysWorked": 3, - "memberRate": 24.46, - "customerRate": 18.76, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.806Z", - "updatedAt": null - }, - { - "id": "1b633124-f62c-4026-b679-213cf5812689", - "resourceBookingId": "51b45f5d-5df2-46d5-9c3d-8a1323df38dd", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-03-28", - "endDate": "2021-04-03", - "daysWorked": 5, - "memberRate": 23.35, - "customerRate": 18.76, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.704Z", - "updatedAt": null - }, - { - "id": "30c89d78-df85-44ed-92b2-b683d8960fc2", - "resourceBookingId": "51b45f5d-5df2-46d5-9c3d-8a1323df38dd", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-04-04", - "endDate": "2021-04-10", - "daysWorked": 5, - "memberRate": 3.96, - "customerRate": 4.95, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.704Z", - "updatedAt": null - }, - { - "id": "531754f3-405e-42eb-932a-c1ce7b9386e7", - "resourceBookingId": "35e1abd8-1890-4664-bb52-aade382d7b66", - "userHandle": "lakshmiaconnmgr", - "projectId": 111, - "startDate": "2021-02-28", - "endDate": "2021-03-06", - "daysWorked": 5, - "memberRate": 3.62, - "customerRate": 10.05, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.695Z", - "updatedAt": null - }, - { - "id": "442b0543-e887-42be-9ff5-5fee825526be", - "resourceBookingId": "0a6799d7-f5d1-456b-8bf1-90619284b295", - "userHandle": "aaaa", - "projectId": 111, - "startDate": "2021-03-14", - "endDate": "2021-03-20", - "daysWorked": 1, - "memberRate": 23.81, - "customerRate": 15.49, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.694Z", - "updatedAt": null - }, - { - "id": "18a73600-5ee8-484b-9747-70ea80ce2bd9", - "resourceBookingId": "7d967fed-9792-4768-98a7-0b644aa84f2e", - "userHandle": "sachin-wipro", - "projectId": 111, - "startDate": "2021-05-16", - "endDate": "2021-05-22", - "daysWorked": 5, - "memberRate": 28.76, - "customerRate": 25.9, - "paymentStatus": "completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.705Z", - "updatedAt": null - }, - { - "id": "5392acee-b504-4720-95fa-dab585acb607", - "resourceBookingId": "0a6799d7-f5d1-456b-8bf1-90619284b295", - "userHandle": "aaaa", - "projectId": 111, - "startDate": "2021-03-07", - "endDate": "2021-03-13", - "daysWorked": 5, - "memberRate": 14.77, - "customerRate": 26.72, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.693Z", - "updatedAt": null - }, - { - "id": "1bbc86c8-8c74-4d78-9706-25c4c99721f3", - "resourceBookingId": "35e1abd8-1890-4664-bb52-aade382d7b66", - "userHandle": "lakshmiaconnmgr", - "projectId": 111, - "startDate": "2021-03-21", - "endDate": "2021-03-27", - "daysWorked": 5, - "memberRate": 3.62, - "customerRate": 25.9, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.695Z", - "updatedAt": null - }, - { - "id": "8b7b49de-29b1-4a0c-bb67-e70cc700fcfd", - "resourceBookingId": "51b45f5d-5df2-46d5-9c3d-8a1323df38dd", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-04-25", - "endDate": "2021-05-01", - "daysWorked": 5, - "memberRate": 24.68, - "customerRate": 23.15, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.704Z", - "updatedAt": null - }, - { - "id": "b8a8b558-0cb9-46d0-ba9d-2222f3fbdb63", - "resourceBookingId": "dc4477ec-07f8-4c8e-a8fe-ffe38dd290fa", - "userHandle": "nskumar278", - "projectId": 111, - "startDate": "2021-09-05", - "endDate": "2021-09-11", - "daysWorked": 5, - "memberRate": 27.42, - "customerRate": 27.91, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.703Z", - "updatedAt": null - }, - { - "id": "f70509c0-baed-4ff1-8022-12441251f7af", - "resourceBookingId": "51b45f5d-5df2-46d5-9c3d-8a1323df38dd", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-03-14", - "endDate": "2021-03-20", - "daysWorked": 2, - "memberRate": 30.24, - "customerRate": 9.88, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.704Z", - "updatedAt": null - }, - { - "id": "da8abd64-2878-4e09-b3b1-41a27a0576d4", - "resourceBookingId": "dc4477ec-07f8-4c8e-a8fe-ffe38dd290fa", - "userHandle": "nskumar278", - "projectId": 111, - "startDate": "2021-09-12", - "endDate": "2021-09-18", - "daysWorked": 5, - "memberRate": 16.86, - "customerRate": 4.05, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.703Z", - "updatedAt": null - }, - { - "id": "b5261ee2-bc4d-4c3a-9d30-6703bfdcfbc5", - "resourceBookingId": "51b45f5d-5df2-46d5-9c3d-8a1323df38dd", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-05-23", - "endDate": "2021-05-29", - "daysWorked": 5, - "memberRate": 24.68, - "customerRate": 27.91, - "paymentStatus": "completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.704Z", - "updatedAt": null - }, - { - "id": "829165cb-0150-48e7-995c-f5b4d1a51d3b", - "resourceBookingId": "dc4477ec-07f8-4c8e-a8fe-ffe38dd290fa", - "userHandle": "nskumar278", - "projectId": 111, - "startDate": "2021-09-26", - "endDate": "2021-10-02", - "daysWorked": 1, - "memberRate": 4.8, - "customerRate": 10.59, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.703Z", - "updatedAt": null - }, - { - "id": "2430fb35-826a-4e9f-8fb6-8dbcb035d505", - "resourceBookingId": "dc4477ec-07f8-4c8e-a8fe-ffe38dd290fa", - "userHandle": "nskumar278", - "projectId": 111, - "startDate": "2021-08-22", - "endDate": "2021-08-28", - "daysWorked": 5, - "memberRate": 6.18, - "customerRate": 4.05, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.703Z", - "updatedAt": null - }, - { - "id": "c7ff2acc-a4c2-4e8b-a905-813dd9d1b293", - "resourceBookingId": "35e1abd8-1890-4664-bb52-aade382d7b66", - "userHandle": "lakshmiaconnmgr", - "projectId": 111, - "startDate": "2021-03-28", - "endDate": "2021-04-03", - "daysWorked": 4, - "memberRate": 24.46, - "customerRate": 4.05, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.695Z", - "updatedAt": null - }, - { - "id": "731db5d7-bb79-45f4-b1b2-9f60d30f7966", - "resourceBookingId": "08f5e4b9-1088-496d-91a7-5b22a3583e3c", - "userHandle": "ritesh_cs", - "projectId": 111, - "startDate": "2021-01-31", - "endDate": "2021-02-06", - "daysWorked": 0, - "memberRate": 27.42, - "customerRate": 4.4, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.775Z", - "updatedAt": null - }, - { - "id": "b682660e-488e-4033-aaf3-ef37248abc90", - "resourceBookingId": "0a6799d7-f5d1-456b-8bf1-90619284b295", - "userHandle": "aaaa", - "projectId": 111, - "startDate": "2021-02-21", - "endDate": "2021-02-27", - "daysWorked": 0, - "memberRate": 24.46, - "customerRate": 2.55, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.693Z", - "updatedAt": null - }, - { - "id": "b4566e24-4fa8-4e82-9950-f62134bb00df", - "resourceBookingId": "dc4477ec-07f8-4c8e-a8fe-ffe38dd290fa", - "userHandle": "nskumar278", - "projectId": 111, - "startDate": "2021-08-29", - "endDate": "2021-09-04", - "daysWorked": 5, - "memberRate": 4.8, - "customerRate": 4.4, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.703Z", - "updatedAt": null - }, - { - "id": "584c931b-da72-416e-a011-eac5fe34f42f", - "resourceBookingId": "dc4477ec-07f8-4c8e-a8fe-ffe38dd290fa", - "userHandle": "nskumar278", - "projectId": 111, - "startDate": "2021-07-25", - "endDate": "2021-07-31", - "daysWorked": 4, - "memberRate": 14.77, - "customerRate": 4.4, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.703Z", - "updatedAt": null - }, - { - "id": "703bc625-9144-4a9f-a91b-445e171d8ea9", - "resourceBookingId": "dc4477ec-07f8-4c8e-a8fe-ffe38dd290fa", - "userHandle": "nskumar278", - "projectId": 111, - "startDate": "2021-09-19", - "endDate": "2021-09-25", - "daysWorked": 5, - "memberRate": 27.42, - "customerRate": 5.77, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.703Z", - "updatedAt": null - }, - { - "id": "c9685cb2-9f4a-497e-ae99-deed7b16cc34", - "resourceBookingId": "35e1abd8-1890-4664-bb52-aade382d7b66", - "userHandle": "lakshmiaconnmgr", - "projectId": 111, - "startDate": "2021-03-07", - "endDate": "2021-03-13", - "daysWorked": 5, - "memberRate": 14.77, - "customerRate": 16.55, - "paymentStatus": "completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.695Z", - "updatedAt": null - }, - { - "id": "428fec88-b305-4816-b5ef-be2b4e91c21a", - "resourceBookingId": "7d967fed-9792-4768-98a7-0b644aa84f2e", - "userHandle": "sachin-wipro", - "projectId": 111, - "startDate": "2021-04-04", - "endDate": "2021-04-10", - "daysWorked": 5, - "memberRate": 8.55, - "customerRate": 16.55, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.705Z", - "updatedAt": null - }, - { - "id": "374e2065-40cb-4651-a55c-81a0675dc00d", - "resourceBookingId": "8173579e-4b3c-418d-a9a1-c999caa38404", - "userHandle": "testcat", - "projectId": 111, - "startDate": "2020-05-17", - "endDate": "2020-05-23", - "daysWorked": 5, - "memberRate": 16.86, - "customerRate": 1.89, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.806Z", - "updatedAt": null - }, - { - "id": "4bfdc253-e39a-4cfa-9a45-2cfc6e93cc6f", - "resourceBookingId": "61f5d474-e41f-490b-ab58-9f983e3d4916", - "userHandle": "sonu628", - "projectId": 111, - "startDate": "2000-04-16", - "endDate": "2000-04-22", - "daysWorked": 5, - "memberRate": 20.16, - "customerRate": 13.92, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.858Z", - "updatedAt": null - }, - { - "id": "0ba1a0b3-7bee-4fe6-9083-d36d8ca9d052", - "resourceBookingId": "35e1abd8-1890-4664-bb52-aade382d7b66", - "userHandle": "lakshmiaconnmgr", - "projectId": 111, - "startDate": "2021-03-14", - "endDate": "2021-03-20", - "daysWorked": 5, - "memberRate": 24.46, - "customerRate": 1.44, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.695Z", - "updatedAt": null - }, - { - "id": "6da7d347-7492-43b1-a49d-8a6e1daf9b22", - "resourceBookingId": "51b45f5d-5df2-46d5-9c3d-8a1323df38dd", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-04-18", - "endDate": "2021-04-24", - "daysWorked": 5, - "memberRate": 25.21, - "customerRate": 1.44, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.704Z", - "updatedAt": null - }, - { - "id": "8c8cedf3-0bb6-4063-8a3a-faf1626da384", - "resourceBookingId": "61f5d474-e41f-490b-ab58-9f983e3d4916", - "userHandle": "sonu628", - "projectId": 111, - "startDate": "2000-04-09", - "endDate": "2000-04-15", - "daysWorked": 5, - "memberRate": 14.77, - "customerRate": 4.95, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.858Z", - "updatedAt": null - }, - { - "id": "10394696-47f4-4761-8d14-ffdf012cde23", - "resourceBookingId": "d38a6223-3f91-4300-9ecb-6e5fee173625", - "userHandle": "nithyaasworld", - "projectId": 111, - "startDate": "2021-07-25", - "endDate": "2021-07-31", - "daysWorked": 5, - "memberRate": 3.62, - "customerRate": 4.95, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.706Z", - "updatedAt": null - }, - { - "id": "64de2244-6fcf-45ac-8a89-a9b3420e4476", - "resourceBookingId": "7d967fed-9792-4768-98a7-0b644aa84f2e", - "userHandle": "sachin-wipro", - "projectId": 111, - "startDate": "2021-04-18", - "endDate": "2021-04-24", - "daysWorked": 5, - "memberRate": 22.57, - "customerRate": 18.23, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.705Z", - "updatedAt": null - }, - { - "id": "2cbe3836-5961-4d58-b8f7-44b84ddf3fc5", - "resourceBookingId": "7d967fed-9792-4768-98a7-0b644aa84f2e", - "userHandle": "sachin-wipro", - "projectId": 111, - "startDate": "2021-04-11", - "endDate": "2021-04-17", - "daysWorked": 5, - "memberRate": 25.21, - "customerRate": 2.64, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.705Z", - "updatedAt": null - }, - { - "id": "bc8611f0-4ac2-49b2-b776-59ed2b759d1d", - "resourceBookingId": "d38a6223-3f91-4300-9ecb-6e5fee173625", - "userHandle": "nithyaasworld", - "projectId": 111, - "startDate": "2021-07-04", - "endDate": "2021-07-10", - "daysWorked": 5, - "memberRate": 16.86, - "customerRate": 5.77, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.706Z", - "updatedAt": null - }, - { - "id": "9b6fc85b-3694-4e6c-9ebf-fe9ccf7881ba", - "resourceBookingId": "d38a6223-3f91-4300-9ecb-6e5fee173625", - "userHandle": "nithyaasworld", - "projectId": 111, - "startDate": "2021-06-13", - "endDate": "2021-06-19", - "daysWorked": 5, - "memberRate": 14.77, - "customerRate": 15.86, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.706Z", - "updatedAt": null - }, - { - "id": "4a2c581f-c626-44cb-8ada-58f0172e8b4d", - "resourceBookingId": "d38a6223-3f91-4300-9ecb-6e5fee173625", - "userHandle": "nithyaasworld", - "projectId": 111, - "startDate": "2021-05-30", - "endDate": "2021-06-05", - "daysWorked": 5, - "memberRate": 28.76, - "customerRate": 15.49, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.706Z", - "updatedAt": null - }, - { - "id": "90d99323-ba57-4a18-b50f-78f387033c70", - "resourceBookingId": "61f5d474-e41f-490b-ab58-9f983e3d4916", - "userHandle": "sonu628", - "projectId": 111, - "startDate": "2000-04-02", - "endDate": "2000-04-08", - "daysWorked": 5, - "memberRate": 24.46, - "customerRate": 25.9, - "paymentStatus": "completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.858Z", - "updatedAt": null - }, - { - "id": "5ddc132b-3205-45e3-bfe9-f1a4f939885e", - "resourceBookingId": "51b45f5d-5df2-46d5-9c3d-8a1323df38dd", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-03-21", - "endDate": "2021-03-27", - "daysWorked": 5, - "memberRate": 21.53, - "customerRate": 25.9, - "paymentStatus": "completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.704Z", - "updatedAt": null - }, - { - "id": "437d4281-43ec-4de3-85e4-51d298388ce5", - "resourceBookingId": "0a6799d7-f5d1-456b-8bf1-90619284b295", - "userHandle": "aaaa", - "projectId": 111, - "startDate": "2021-02-28", - "endDate": "2021-03-06", - "daysWorked": 5, - "memberRate": 26.19, - "customerRate": 28.03, - "paymentStatus": "completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.693Z", - "updatedAt": null - }, - { - "id": "a35bb205-361b-4b2d-9828-55501ae9d190", - "resourceBookingId": "d38a6223-3f91-4300-9ecb-6e5fee173625", - "userHandle": "nithyaasworld", - "projectId": 111, - "startDate": "2021-07-11", - "endDate": "2021-07-17", - "daysWorked": 5, - "memberRate": 27.42, - "customerRate": 28.03, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.706Z", - "updatedAt": null - }, - { - "id": "40ae1e8f-24be-4b51-885c-51f08e15f0df", - "resourceBookingId": "08f5e4b9-1088-496d-91a7-5b22a3583e3c", - "userHandle": "ritesh_cs", - "projectId": 111, - "startDate": "2021-01-24", - "endDate": "2021-01-30", - "daysWorked": 5, - "memberRate": 16.86, - "customerRate": 28.03, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.775Z", - "updatedAt": null - }, - { - "id": "e19bddc5-38ac-43c9-8273-bc9069de26b3", - "resourceBookingId": "61f5d474-e41f-490b-ab58-9f983e3d4916", - "userHandle": "sonu628", - "projectId": 111, - "startDate": "2000-04-23", - "endDate": "2000-04-29", - "daysWorked": 5, - "memberRate": 6.18, - "customerRate": 9.12, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.858Z", - "updatedAt": null - }, - { - "id": "bf61dad8-841a-4913-93f6-b2ae516b8b11", - "resourceBookingId": "d38a6223-3f91-4300-9ecb-6e5fee173625", - "userHandle": "nithyaasworld", - "projectId": 111, - "startDate": "2021-06-06", - "endDate": "2021-06-12", - "daysWorked": 5, - "memberRate": 6.18, - "customerRate": 4.4, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.706Z", - "updatedAt": null - }, - { - "id": "93b0f03b-219b-41d5-bb1e-f125651b1b0e", - "resourceBookingId": "8173579e-4b3c-418d-a9a1-c999caa38404", - "userHandle": "testcat", - "projectId": 111, - "startDate": "2020-05-03", - "endDate": "2020-05-09", - "daysWorked": 5, - "memberRate": 24.46, - "customerRate": 1.89, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.806Z", - "updatedAt": null - }, - { - "id": "93e69f99-cb1c-418e-aa41-ab2181b4bcfd", - "resourceBookingId": "d38a6223-3f91-4300-9ecb-6e5fee173625", - "userHandle": "nithyaasworld", - "projectId": 111, - "startDate": "2021-06-27", - "endDate": "2021-07-03", - "daysWorked": 5, - "memberRate": 17.2, - "customerRate": 13.92, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.706Z", - "updatedAt": null - }, - { - "id": "619a0835-730f-47ec-820e-3959821aec51", - "resourceBookingId": "7d967fed-9792-4768-98a7-0b644aa84f2e", - "userHandle": "sachin-wipro", - "projectId": 111, - "startDate": "2021-05-30", - "endDate": "2021-06-05", - "daysWorked": 1, - "memberRate": 4.8, - "customerRate": 13.92, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.705Z", - "updatedAt": null - }, - { - "id": "558b4685-4932-492e-99e1-2830c10f8275", - "resourceBookingId": "7d967fed-9792-4768-98a7-0b644aa84f2e", - "userHandle": "sachin-wipro", - "projectId": 111, - "startDate": "2021-04-25", - "endDate": "2021-05-01", - "daysWorked": 5, - "memberRate": 4.8, - "customerRate": 13.92, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.705Z", - "updatedAt": null - }, - { - "id": "84431dac-019d-4987-9817-15f5f000d2db", - "resourceBookingId": "61f5d474-e41f-490b-ab58-9f983e3d4916", - "userHandle": "sonu628", - "projectId": 111, - "startDate": "2000-03-26", - "endDate": "2000-04-01", - "daysWorked": 5, - "memberRate": 27.42, - "customerRate": 4.05, - "paymentStatus": "partially-completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.858Z", - "updatedAt": null - }, - { - "id": "f575df2d-f170-4251-9042-b17ca5e99ca5", - "resourceBookingId": "51b45f5d-5df2-46d5-9c3d-8a1323df38dd", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-05-02", - "endDate": "2021-05-08", - "daysWorked": 5, - "memberRate": 4.82, - "customerRate": 4.05, - "paymentStatus": "completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.704Z", - "updatedAt": null - }, - { - "id": "3829c216-ef3f-466c-93da-3a88a961442f", - "resourceBookingId": "51b45f5d-5df2-46d5-9c3d-8a1323df38dd", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-05-09", - "endDate": "2021-05-15", - "daysWorked": 5, - "memberRate": 6.18, - "customerRate": 4.4, - "paymentStatus": "cancelled", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.704Z", - "updatedAt": null - }, - { - "id": "bd54bcaf-5278-467f-a166-865048774d0e", - "resourceBookingId": "a098e8d8-ce5b-47d9-afee-38b050d16745", - "userHandle": "TCConnCopilot", - "projectId": 111, - "startDate": "2021-04-25", - "endDate": "2021-05-01", - "daysWorked": 5, - "memberRate": 16.86, - "customerRate": 1.89, - "paymentStatus": "partially-completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.696Z", - "updatedAt": null - }, - { - "id": "7f8f3a25-b1d2-4e36-a543-fcaae57a32f5", - "resourceBookingId": "dc4477ec-07f8-4c8e-a8fe-ffe38dd290fa", - "userHandle": "nskumar278", - "projectId": 111, - "startDate": "2021-08-15", - "endDate": "2021-08-21", - "daysWorked": 5, - "memberRate": 8.13, - "customerRate": 1.89, - "paymentStatus": "cancelled", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.703Z", - "updatedAt": null - }, - { - "id": "6e1dc024-d437-4d9f-92e9-561db713a19a", - "resourceBookingId": "dc4477ec-07f8-4c8e-a8fe-ffe38dd290fa", - "userHandle": "nskumar278", - "projectId": 111, - "startDate": "2021-08-08", - "endDate": "2021-08-14", - "daysWorked": 5, - "memberRate": 3.62, - "customerRate": 18.14, - "paymentStatus": "partially-completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.703Z", - "updatedAt": null - }, - { - "id": "c5f6ae8f-2976-46d1-bd66-58eeb58ca045", - "resourceBookingId": "d38a6223-3f91-4300-9ecb-6e5fee173625", - "userHandle": "nithyaasworld", - "projectId": 111, - "startDate": "2021-06-20", - "endDate": "2021-06-26", - "daysWorked": 5, - "memberRate": 25.41, - "customerRate": 13.92, - "paymentStatus": "cancelled", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.706Z", - "updatedAt": null - }, - { - "id": "0f480aa7-1c5d-42fe-aad6-c774e58c2e17", - "resourceBookingId": "7d967fed-9792-4768-98a7-0b644aa84f2e", - "userHandle": "sachin-wipro", - "projectId": 111, - "startDate": "2021-05-09", - "endDate": "2021-05-15", - "daysWorked": 5, - "memberRate": 4.82, - "customerRate": 4.95, - "paymentStatus": "cancelled", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.705Z", - "updatedAt": null - }, - { - "id": "098ca553-6ea4-4cae-a18e-762f47e93d82", - "resourceBookingId": "51b45f5d-5df2-46d5-9c3d-8a1323df38dd", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-05-16", - "endDate": "2021-05-22", - "daysWorked": 5, - "memberRate": 3.96, - "customerRate": 4.95, - "paymentStatus": "completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.704Z", - "updatedAt": null - }, - { - "id": "729a2396-2b33-4185-8ce8-d929dbf4a472", - "resourceBookingId": "7d967fed-9792-4768-98a7-0b644aa84f2e", - "userHandle": "sachin-wipro", - "projectId": 111, - "startDate": "2021-05-23", - "endDate": "2021-05-29", - "daysWorked": 5, - "memberRate": 17.2, - "customerRate": 4.95, - "paymentStatus": "completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.705Z", - "updatedAt": null - }, - { - "id": "9bfbd49a-e6bb-45c3-965d-ed4f95f0878a", - "resourceBookingId": "d38a6223-3f91-4300-9ecb-6e5fee173625", - "userHandle": "nithyaasworld", - "projectId": 111, - "startDate": "2021-07-18", - "endDate": "2021-07-24", - "daysWorked": 5, - "memberRate": 23.35, - "customerRate": 5.77, - "paymentStatus": "completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.706Z", - "updatedAt": null - }, - { - "id": "dcc310cf-ea4a-4637-9bf5-5baaabea67b3", - "resourceBookingId": "d38a6223-3f91-4300-9ecb-6e5fee173625", - "userHandle": "nithyaasworld", - "projectId": 111, - "startDate": "2021-05-23", - "endDate": "2021-05-29", - "daysWorked": 4, - "memberRate": 14.77, - "customerRate": 1.44, - "paymentStatus": "partially-completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.706Z", - "updatedAt": null - }, - { - "id": "309f65e8-b0b5-4cbb-ba60-9b36ccba4bd5", - "resourceBookingId": "51b45f5d-5df2-46d5-9c3d-8a1323df38dd", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-04-11", - "endDate": "2021-04-17", - "daysWorked": 5, - "memberRate": 14.77, - "customerRate": 15.49, - "paymentStatus": "completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.704Z", - "updatedAt": null - }, - { - "id": "1e2c732e-ac1f-47ad-939b-22b2078124e5", - "resourceBookingId": "7d967fed-9792-4768-98a7-0b644aa84f2e", - "userHandle": "sachin-wipro", - "projectId": 111, - "startDate": "2021-03-28", - "endDate": "2021-04-03", - "daysWorked": 5, - "memberRate": 17.2, - "customerRate": 15.49, - "paymentStatus": "partially-completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.705Z", - "updatedAt": null - }, - { - "id": "2463003f-ee60-4aba-b989-b63babcd9b8b", - "resourceBookingId": "7d967fed-9792-4768-98a7-0b644aa84f2e", - "userHandle": "sachin-wipro", - "projectId": 111, - "startDate": "2021-05-02", - "endDate": "2021-05-08", - "daysWorked": 5, - "memberRate": 4.8, - "customerRate": 15.49, - "paymentStatus": "cancelled", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.705Z", - "updatedAt": null - }, - { - "id": "6a1180b8-38af-4bd0-8a5f-a60626e0bc95", - "resourceBookingId": "8173579e-4b3c-418d-a9a1-c999caa38404", - "userHandle": "testcat", - "projectId": 111, - "startDate": "2020-04-26", - "endDate": "2020-05-02", - "daysWorked": 5, - "memberRate": 26.19, - "customerRate": 28.03, - "paymentStatus": "completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.806Z", - "updatedAt": null - }, - { - "id": "b21f921a-4b23-423d-814b-d31f5cba7f64", - "resourceBookingId": "35e1abd8-1890-4664-bb52-aade382d7b66", - "userHandle": "lakshmiaconnmgr", - "projectId": 111, - "startDate": "2021-02-21", - "endDate": "2021-02-27", - "daysWorked": 2, - "memberRate": 15.77, - "customerRate": 28.03, - "paymentStatus": "partially-completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.695Z", - "updatedAt": null - }, - { - "id": "4db82b0b-4d6f-4e9a-b957-ab405b2c2df2", - "resourceBookingId": "8173579e-4b3c-418d-a9a1-c999caa38404", - "userHandle": "testcat", - "projectId": 111, - "startDate": "2020-05-10", - "endDate": "2020-05-16", - "daysWorked": 5, - "memberRate": 24.46, - "customerRate": 28.03, - "paymentStatus": "partially-completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-04-21T20:24:52.806Z", - "updatedAt": null - } - ], - "WorkPeriodPayment": [ - { - "id": "2c488b36-0868-4db6-8978-20b1ce174496", - "workPeriodId": "07783b60-b726-41c2-8955-7766a27c1ec5", - "challengeId": "0de972b8-6cc9-4625-9760-1c77561c29e9", - "amount": 200, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "d0f61c2d-271c-48b2-8416-50c1c32ad32b", - "workPeriodId": "098ca553-6ea4-4cae-a18e-762f47e93d82", - "challengeId": "aa8c1945-904c-42a7-9b00-1e4f9a49dcdb", - "amount": 450, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.361Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "90bb43bd-d9e7-4eab-a46b-ab03338be11a", - "workPeriodId": "0ba1a0b3-7bee-4fe6-9083-d36d8ca9d052", - "challengeId": "8b6f4040-d7ae-4264-b60b-b1171c9365e4", - "amount": 400, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.390Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "58681a3e-2266-4153-afd5-7be218966da2", - "workPeriodId": "0f480aa7-1c5d-42fe-aad6-c774e58c2e17", - "challengeId": "2bafdd9a-ab4a-4624-8a06-7b1adfb8dac2", - "amount": 1200, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.326Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "b342d615-d36d-48b2-a3e8-1f498b58ab68", - "workPeriodId": "10394696-47f4-4761-8d14-ffdf012cde23", - "challengeId": "0de972b8-6cc9-4625-9760-1c77561c29e9", - "amount": 400, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-16T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "63cec011-7714-4ab6-944f-e76b9fa2bd1a", - "workPeriodId": "18a73600-5ee8-484b-9747-70ea80ce2bd9", - "challengeId": "1bdf092e-e117-4d42-bb5d-e49816171c0d", - "amount": 500, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.523Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "8a0c0877-b88a-420b-a3f7-645c18793b4b", - "workPeriodId": "1b633124-f62c-4026-b679-213cf5812689", - "challengeId": "4332acc6-3a33-4b7b-af4c-bd2c1ffcfb8e", - "amount": 1400, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.325Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "04ef47ac-8f4d-402f-b9fd-48b2b1d9003d", - "workPeriodId": "1bbc86c8-8c74-4d78-9706-25c4c99721f3", - "challengeId": "fa909769-f5f7-41fb-94bb-80db62e21a5e", - "amount": 800, - "status": "cancelled", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.326Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "84c1c956-e03d-4e19-a5a8-a4086c9658eb", - "workPeriodId": "1e2c732e-ac1f-47ad-939b-22b2078124e5", - "challengeId": "604910b2-7644-4b9d-a185-0054e61c83fc", - "amount": 1800, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.324Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "c051c3da-34bb-4deb-8921-04e5d2fa0bcc", - "workPeriodId": "2430fb35-826a-4e9f-8fb6-8dbcb035d505", - "challengeId": "25fd805c-49cd-443f-ba0d-813c33a6d8ad", - "amount": 400, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.390Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "cc13acfd-fc06-40e5-aefe-5050429ee4f2", - "workPeriodId": "2463003f-ee60-4aba-b989-b63babcd9b8b", - "challengeId": "0de972b8-6cc9-4625-9760-1c77561c29e9", - "amount": 1200, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "58c6431a-061a-41bd-a3bd-c8effba62e11", - "workPeriodId": "2cbe3836-5961-4d58-b8f7-44b84ddf3fc5", - "challengeId": "0de972b8-6cc9-4625-9760-1c77561c29e9", - "amount": 900, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "efe0e4b5-e6dd-4c1a-a0e5-1f5e0ff91df9", - "workPeriodId": "309f65e8-b0b5-4cbb-ba60-9b36ccba4bd5", - "challengeId": "01332fa9-0505-416d-9084-39b136dbd300", - "amount": 1200, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.390Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "298ac627-e6d7-47b0-ad1d-83de291addf7", - "workPeriodId": "30c89d78-df85-44ed-92b2-b683d8960fc2", - "challengeId": "f7e40a0c-a302-4989-a3bd-7587fee3c367", - "amount": 300, - "status": "cancelled", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-16T19:13:23.714Z" - }, - { - "id": "bf80eca9-8323-4f96-9a3b-8553d6f1e0ce", - "workPeriodId": "374e2065-40cb-4651-a55c-81a0675dc00d", - "challengeId": "03c69d02-9188-436e-b4c9-08f39e46f413", - "amount": 800, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.390Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "338d2d7d-2c59-4513-b560-55e8982fd7c7", - "workPeriodId": "3829c216-ef3f-466c-93da-3a88a961442f", - "challengeId": "2a0db660-def2-4117-90e8-01cd1271c726", - "amount": 1400, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.390Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "491f6e93-e80f-46eb-9637-ee84fe7ace9b", - "workPeriodId": "40ae1e8f-24be-4b51-885c-51f08e15f0df", - "challengeId": "c2b7a2cc-4633-4f3a-ab39-f7122a6890b6", - "amount": 1600, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.390Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "cb94d710-fa3c-4333-b8a9-39ec053a665d", - "workPeriodId": "428fec88-b305-4816-b5ef-be2b4e91c21a", - "challengeId": "93f594e9-5396-416a-8212-87bee614a38f", - "amount": 1200, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "155eaab2-ef74-484c-b1d7-6534dde58b66", - "workPeriodId": "437d4281-43ec-4de3-85e4-51d298388ce5", - "challengeId": "5ba2d9df-b3ba-4f5f-bee5-228eb46b2c37", - "amount": 600, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.361Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "11c07a5c-a077-4719-83ba-c48db7e63c01", - "workPeriodId": "442b0543-e887-42be-9ff5-5fee825526be", - "challengeId": "d7c21199-a435-41b8-a2ac-5e5bdd021c5e", - "amount": 1400, - "status": "cancelled", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "4ce8cf4a-2585-4454-81d4-e5446c9a3f75", - "workPeriodId": "4a2c581f-c626-44cb-8ada-58f0172e8b4d", - "challengeId": "ee9bcdd0-0d0d-4414-88cb-d14255f996a5", - "amount": 350, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-16T19:13:23.714Z" - }, - { - "id": "8ff6188f-b4e6-4f6e-b2d4-2a622ae50c4f", - "workPeriodId": "4bfdc253-e39a-4cfa-9a45-2cfc6e93cc6f", - "challengeId": "f151a5a1-6b28-4f59-bdde-6e73498faa84", - "amount": 450, - "status": "cancelled", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.326Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "5192e519-30f5-42d6-877f-5c2b786d91c7", - "workPeriodId": "4db82b0b-4d6f-4e9a-b957-ab405b2c2df2", - "challengeId": "93f594e9-5396-416a-8212-87bee614a38f", - "amount": 600, - "status": "cancelled", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-16T19:13:23.714Z" - }, - { - "id": "8ca85158-a850-4cae-97f0-5873a9225ff8", - "workPeriodId": "531754f3-405e-42eb-932a-c1ce7b9386e7", - "challengeId": "b42b8f40-9a4d-45d1-814e-59da5c7a1fd9", - "amount": 1200, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "4a81c6a3-18d9-441b-ae64-3aa9def336ef", - "workPeriodId": "5392acee-b504-4720-95fa-dab585acb607", - "challengeId": "78154e5c-feed-4dbf-84ec-775395139211", - "amount": 200, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-16T19:13:23.714Z" - }, - { - "id": "a26b8cae-a1df-4843-a192-0735f235bf78", - "workPeriodId": "558b4685-4932-492e-99e1-2830c10f8275", - "challengeId": "0de972b8-6cc9-4625-9760-1c77561c29e9", - "amount": 600, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "a94b6f43-de89-4fd2-9a1d-d8a2cce02702", - "workPeriodId": "584c931b-da72-416e-a011-eac5fe34f42f", - "challengeId": "d55fae30-3072-462e-b8bd-eb2eec664e65", - "amount": 1600, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.324Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "eb63dfc7-14d7-4cae-a25e-3e33a04601cf", - "workPeriodId": "5ddc132b-3205-45e3-bfe9-f1a4f939885e", - "challengeId": "0de972b8-6cc9-4625-9760-1c77561c29e9", - "amount": 300, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "d827ed01-5153-4774-8450-bace2521a6d1", - "workPeriodId": "619a0835-730f-47ec-820e-3959821aec51", - "challengeId": "d40cb9c5-b3bd-45a1-802c-a50fefb020b9", - "amount": 1000, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.326Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "f950e52e-5a52-4ee8-af5f-e6c42e9b400b", - "workPeriodId": "64de2244-6fcf-45ac-8a89-a9b3420e4476", - "challengeId": "6c04d456-bb38-43c8-a538-f5cc6caaa540", - "amount": 800, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "283f9c3b-7f0a-4792-9661-ced7cbdb340d", - "workPeriodId": "6a1180b8-38af-4bd0-8a5f-a60626e0bc95", - "challengeId": "9a995b78-0542-41be-a926-f3df57c2f873", - "amount": 400, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.361Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "09ea6f03-4489-496c-817c-e9279a58c0ad", - "workPeriodId": "6da7d347-7492-43b1-a49d-8a6e1daf9b22", - "challengeId": "78154e5c-feed-4dbf-84ec-775395139211", - "amount": 400, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "7d782624-243e-497b-90ca-cc00ae29ebf2", - "workPeriodId": "6e1dc024-d437-4d9f-92e9-561db713a19a", - "challengeId": "afcbc3bc-9077-4978-bf9c-acddb48e6518", - "amount": 800, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.324Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "11918ff9-5f18-445d-ae90-762b3ebe44ca", - "workPeriodId": "6ed667a5-275e-4f27-984b-34dff5f8a1ff", - "challengeId": "c89d357f-afd1-4757-824f-2cf94e2890c6", - "amount": 250, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.361Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "9fe78619-5c1c-4b22-8d6f-ebf9a9502432", - "workPeriodId": "703bc625-9144-4a9f-a91b-445e171d8ea9", - "challengeId": "6c04d456-bb38-43c8-a538-f5cc6caaa540", - "amount": 400, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "7cf85ce0-ef8c-4152-8623-9a1325db6daa", - "workPeriodId": "729a2396-2b33-4185-8ce8-d929dbf4a472", - "challengeId": "fbf8efda-fe4b-4b72-99dd-573b79d27bd8", - "amount": 200, - "status": "cancelled", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.390Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "e8aee371-1f26-4475-8459-9c134fa6f3c6", - "workPeriodId": "731db5d7-bb79-45f4-b1b2-9f60d30f7966", - "challengeId": "ee9bcdd0-0d0d-4414-88cb-d14255f996a5", - "amount": 700, - "status": "cancelled", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "dec8615b-c823-4fff-bc77-fa0614b8d09c", - "workPeriodId": "7f8f3a25-b1d2-4e36-a543-fcaae57a32f5", - "challengeId": "fe3e4056-82e3-4df4-b699-a1f3d8ab5e53", - "amount": 700, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.390Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "c4e316ca-a46f-4fbc-96a6-6fe6665c3b49", - "workPeriodId": "829165cb-0150-48e7-995c-f5b4d1a51d3b", - "challengeId": "d937a822-4a9b-40db-afe6-fe1285e3f7f9", - "amount": 900, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.324Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "12b0bcfc-59ae-478c-bb32-cb18d1d0244c", - "workPeriodId": "84431dac-019d-4987-9817-15f5f000d2db", - "challengeId": "a07ea88f-c52b-4023-8730-c4364efd0dd2", - "amount": 600, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.361Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "8c9ed23c-12c7-4a8b-8e91-6fd3b3fc43a4", - "workPeriodId": "8b7b49de-29b1-4a0c-bb67-e70cc700fcfd", - "challengeId": "0de972b8-6cc9-4625-9760-1c77561c29e9", - "amount": 450, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "621d793b-0ead-4d99-a119-e32af526ece7", - "workPeriodId": "8c8cedf3-0bb6-4063-8a3a-faf1626da384", - "challengeId": "ad04dcee-9a3c-4345-b0b5-783ce98e94b2", - "amount": 900, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.390Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "56cb3aef-531b-4a26-817e-4b9629212c9a", - "workPeriodId": "90d99323-ba57-4a18-b50f-78f387033c70", - "challengeId": "0de972b8-6cc9-4625-9760-1c77561c29e9", - "amount": 450, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "a1ffc559-703a-4c35-8ba3-73a03c3454af", - "workPeriodId": "93b0f03b-219b-41d5-bb1e-f125651b1b0e", - "challengeId": "9fca67f9-3641-4982-8a82-e86b1850efc8", - "amount": 600, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.326Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "a5904ae3-7524-4709-b798-20c4a2193f10", - "workPeriodId": "93e69f99-cb1c-418e-aa41-ab2181b4bcfd", - "challengeId": "ed56a1c7-a965-4ab6-9cf0-0d7c14cd7db3", - "amount": 400, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.361Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "821b38e8-657d-4f72-b27a-dd975c506ced", - "workPeriodId": "9b6fc85b-3694-4e6c-9ebf-fe9ccf7881ba", - "challengeId": "61f5185b-4ee1-4df3-9c6f-52f5bf0cbfc5", - "amount": 500, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.361Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "2417df2c-8e29-4868-aeb8-5ec0ff67713e", - "workPeriodId": "9bfbd49a-e6bb-45c3-965d-ed4f95f0878a", - "challengeId": "f01ba8da-c891-472d-9ed6-74b91ed9b407", - "amount": 400, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "5116d4ea-2c1a-4be6-b335-bd531b8d5734", - "workPeriodId": "a35bb205-361b-4b2d-9828-55501ae9d190", - "challengeId": "b42b8f40-9a4d-45d1-814e-59da5c7a1fd9", - "amount": 600, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-16T19:13:23.714Z" - }, - { - "id": "8fdfe90b-2155-4a71-a610-4689106ced34", - "workPeriodId": "b21f921a-4b23-423d-814b-d31f5cba7f64", - "challengeId": "963784fd-4174-458c-a311-a5b8333d66ab", - "amount": 300, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.361Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "dd9c842a-f577-43c2-8f17-cc940f26f0d8", - "workPeriodId": "b4566e24-4fa8-4e82-9950-f62134bb00df", - "challengeId": "b67cbbc5-3384-4ea6-8504-d3fe83a6e238", - "amount": 700, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.326Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "7e41ca7f-d3c0-4d70-9daf-ee52c93695c7", - "workPeriodId": "b5261ee2-bc4d-4c3a-9d30-6703bfdcfbc5", - "challengeId": "9d49655f-99f6-4e67-9819-a117ef746272", - "amount": 500, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.361Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "b8d44e83-922c-4c2c-8567-5764794e03f3", - "workPeriodId": "b682660e-488e-4033-aaf3-ef37248abc90", - "challengeId": "d7c21199-a435-41b8-a2ac-5e5bdd021c5e", - "amount": 700, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-16T19:13:23.714Z" - }, - { - "id": "c117de76-965f-4069-aefb-8dd97f95112c", - "workPeriodId": "b8a8b558-0cb9-46d0-ba9d-2222f3fbdb63", - "challengeId": "59d08064-e1b4-4e78-82d8-b2d7ffa6f0b1", - "amount": 900, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.523Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "fda2013c-da68-4601-8d13-d0f4cffd081c", - "workPeriodId": "bc8611f0-4ac2-49b2-b776-59ed2b759d1d", - "challengeId": "0de972b8-6cc9-4625-9760-1c77561c29e9", - "amount": 800, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "be07bfc8-032c-41f7-a247-a4724f65617c", - "workPeriodId": "bd54bcaf-5278-467f-a166-865048774d0e", - "challengeId": "45e33cee-7c83-4288-8187-6858b0c759eb", - "amount": 800, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.361Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "efa8cd14-c93f-4e6c-be53-596f02db8c96", - "workPeriodId": "bf61dad8-841a-4913-93f6-b2ae516b8b11", - "challengeId": "0de972b8-6cc9-4625-9760-1c77561c29e9", - "amount": 800, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "eb9c9a83-fe94-4f51-b9f0-c2d9671e87f6", - "workPeriodId": "c5f6ae8f-2976-46d1-bd66-58eeb58ca045", - "challengeId": "68e6a32e-6c31-4266-ba3b-9d2f63be4274", - "amount": 900, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.361Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "c20796c8-76b0-40e8-9bee-db406c93daaf", - "workPeriodId": "c7ff2acc-a4c2-4e8b-a905-813dd9d1b293", - "challengeId": "513ab4c6-0aa4-482b-a184-87f8447b145b", - "amount": 1800, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.390Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "8fad32f7-cb40-416f-ab24-86cf1c7870ef", - "workPeriodId": "c9685cb2-9f4a-497e-ae99-deed7b16cc34", - "challengeId": "f7e40a0c-a302-4989-a3bd-7587fee3c367", - "amount": 600, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "310384cb-6e85-4f7d-a6d2-8c03ca27871d", - "workPeriodId": "d049e7bc-c827-482b-9ec6-b87d9289d1cd", - "challengeId": "0de972b8-6cc9-4625-9760-1c77561c29e9", - "amount": 900, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "2a122a93-e778-4eb9-868c-5f38b8b9bb4e", - "workPeriodId": "da8abd64-2878-4e09-b3b1-41a27a0576d4", - "challengeId": "25a7c442-427b-4b6d-9c41-112dbd420c0d", - "amount": 800, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "767897c3-bbc2-4b1a-9843-5a6bd82e29b0", - "workPeriodId": "dcc310cf-ea4a-4637-9bf5-5baaabea67b3", - "challengeId": "61044188-965e-40df-94c4-9fa9a9499b25", - "amount": 1000, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.523Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "50f7a6fa-a04d-4873-8fd0-037be2f14425", - "workPeriodId": "e19bddc5-38ac-43c9-8273-bc9069de26b3", - "challengeId": "419fbb3d-921d-4613-a87e-337f38d21885", - "amount": 600, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.523Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "11ea987f-f218-4d2b-b656-0741208539f2", - "workPeriodId": "f575df2d-f170-4251-9042-b17ca5e99ca5", - "challengeId": "0de972b8-6cc9-4625-9760-1c77561c29e9", - "amount": 600, - "status": "completed", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-16T19:13:23.714Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - }, - { - "id": "2519d7f9-81c8-448c-aec2-aa78d6d6a962", - "workPeriodId": "f70509c0-baed-4ff1-8022-12441251f7af", - "challengeId": "fd051497-6050-4edf-b8f7-780128f64dc5", - "amount": 300, - "status": "cancelled", - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-04-14T22:01:45.324Z", - "updatedAt": "2021-04-21T20:19:56.934Z" - } - ] -} + ] +} \ No newline at end of file diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 52000d57..90ea3a3c 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": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -7366,8 +7366,13 @@ "listen": "test", "script": { "exec": [ - "var data = JSON.parse(responseBody);\r", - "postman.setEnvironmentVariable(\"jobIdCreatedByM2M\",data.id);" + "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(\"jobIdCreatedByM2M\", response.id);\r", + " }\r", + "});" ], "type": "text/javascript" } @@ -7683,6 +7688,55 @@ }, "response": [] }, + { + "name": "create resource booking with m2m all", + "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(\"resourceBookingIdCreatedByM2M\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_all_resource_booking}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobIdCreatedByM2M}}\",\r\n \"startDate\": \"2020-09-27\",\r\n \"endDate\": \"2020-10-27\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 10,\r\n \"rateType\": \"hourly\",\r\n \"billingAccountId\": 80000071\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/resourceBookings", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ] + } + }, + "response": [] + }, { "name": "create resource booking with connect user", "event": [ @@ -8065,7 +8119,7 @@ "response": [] }, { - "name": "search resource bookings with booking manager", + "name": "get resource booking with parameters 1", "event": [ { "listen": "test", @@ -8073,6 +8127,10 @@ "exec": [ "pm.test('Status code is 200', function () {\r", " pm.response.to.have.status(200);\r", + " const response = pm.response.json()\r", + " _.each(['id','projectId','status','startDate','endDate','billingAccountId','userId','jobId','rateType','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt'], field => {\r", + " pm.expect(response).to.have.property(field)\r", + " })\r", "});" ], "type": "text/javascript" @@ -8089,58 +8147,18 @@ } ], "url": { - "raw": "{{URL}}/resourceBookings", + "raw": "{{URL}}/resourceBookings/{{resourceBookingId}}?fields=id,projectId,status,startDate,endDate,billingAccountId,userId,jobId,rateType,memberRate,customerRate,createdBy,updatedBy,createdAt,updatedAt,deletedAt", "host": [ "{{URL}}" ], "path": [ - "resourceBookings" + "resourceBookings", + "{{resourceBookingId}}" ], "query": [ { - "key": "page", - "value": "1", - "disabled": true - }, - { - "key": "perPage", - "value": "5", - "disabled": true - }, - { - "key": "sortBy", - "value": "id", - "disabled": true - }, - { - "key": "sortOrder", - "value": "desc", - "disabled": true - }, - { - "key": "startDate", - "value": "2020-09-27", - "disabled": true - }, - { - "key": "endDate", - "value": "2020-09-27", - "disabled": true - }, - { - "key": "rateType", - "value": "hourly", - "disabled": true - }, - { - "key": "status", - "value": "assigned", - "disabled": true - }, - { - "key": "projectIds", - "value": "111, 16705", - "disabled": true + "key": "fields", + "value": "id,projectId,status,startDate,endDate,billingAccountId,userId,jobId,rateType,memberRate,customerRate,createdBy,updatedBy,createdAt,updatedAt,deletedAt" } ] } @@ -8148,7 +8166,7 @@ "response": [] }, { - "name": "search resource bookings with m2m all", + "name": "get resource booking with parameters 1 fromDb", "event": [ { "listen": "test", @@ -8156,6 +8174,10 @@ "exec": [ "pm.test('Status code is 200', function () {\r", " pm.response.to.have.status(200);\r", + " const response = pm.response.json()\r", + " _.each(['id','projectId','status','startDate','endDate','billingAccountId','userId','jobId','rateType','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt'], field => {\r", + " pm.expect(response).to.have.property(field)\r", + " })\r", "});" ], "type": "text/javascript" @@ -8168,57 +8190,26 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_m2m_all_resource_booking}}" + "value": "Bearer {{token_bookingManager}}" } ], "url": { - "raw": "{{URL}}/resourceBookings", + "raw": "{{URL}}/resourceBookings/{{resourceBookingId}}?fields=id,projectId,status,startDate,endDate,billingAccountId,userId,jobId,rateType,memberRate,customerRate,createdBy,updatedBy,createdAt,updatedAt,deletedAt&fromDb=true", "host": [ "{{URL}}" ], "path": [ - "resourceBookings" + "resourceBookings", + "{{resourceBookingId}}" ], "query": [ { - "key": "page", - "value": "1", - "disabled": true - }, - { - "key": "perPage", - "value": "5", - "disabled": true - }, - { - "key": "sortBy", - "value": "id", - "disabled": true - }, - { - "key": "sortOrder", - "value": "desc", - "disabled": true - }, - { - "key": "startDate", - "value": "2020-09-27", - "disabled": true - }, - { - "key": "endDate", - "value": "2020-09-27", - "disabled": true - }, - { - "key": "rateType", - "value": "hourly", - "disabled": true + "key": "fields", + "value": "id,projectId,status,startDate,endDate,billingAccountId,userId,jobId,rateType,memberRate,customerRate,createdBy,updatedBy,createdAt,updatedAt,deletedAt" }, { - "key": "status", - "value": "assigned", - "disabled": true + "key": "fromDb", + "value": "true" } ] } @@ -8226,7 +8217,7 @@ "response": [] }, { - "name": "search resource bookings with connect user", + "name": "get resource booking with parameters 2", "event": [ { "listen": "test", @@ -8234,6 +8225,13 @@ "exec": [ "pm.test('Status code is 200', function () {\r", " pm.response.to.have.status(200);\r", + " const response = pm.response.json()\r", + " _.each(['id','projectId','status','startDate','endDate','billingAccountId','userId','jobId','rateType','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt','workPeriods'], field => {\r", + " pm.expect(response).to.have.property(field)\r", + " })\r", + " _.each(['id','projectId','paymentStatus','startDate','endDate','userHandle','resourceBookingId','daysWorked','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt'], field => {\r", + " pm.expect(response.workPeriods[0]).to.have.property(field)\r", + " })\r", "});" ], "type": "text/javascript" @@ -8246,51 +8244,22 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_connectUser}}" + "value": "Bearer {{token_bookingManager}}" } ], "url": { - "raw": "{{URL}}/resourceBookings?sortOrder=desc", + "raw": "{{URL}}/resourceBookings/{{resourceBookingId}}?fields=workPeriods,id,projectId,status,startDate,endDate,billingAccountId,userId,jobId,rateType,memberRate,customerRate,createdBy,updatedBy,createdAt,updatedAt,deletedAt", "host": [ "{{URL}}" ], "path": [ - "resourceBookings" + "resourceBookings", + "{{resourceBookingId}}" ], "query": [ { - "key": "page", - "value": "4", - "disabled": true - }, - { - "key": "perPage", - "value": "3", - "disabled": true - }, - { - "key": "sortBy", - "value": "id", - "disabled": true - }, - { - "key": "startDate", - "value": "2020-09-27", - "disabled": true - }, - { - "key": "endDate", - "value": "2020-09-27", - "disabled": true - }, - { - "key": "rateType", - "value": "hourly", - "disabled": true - }, - { - "key": "sortOrder", - "value": "desc" + "key": "fields", + "value": "workPeriods,id,projectId,status,startDate,endDate,billingAccountId,userId,jobId,rateType,memberRate,customerRate,createdBy,updatedBy,createdAt,updatedAt,deletedAt" } ] } @@ -8298,13 +8267,22 @@ "response": [] }, { - "name": "search resource bookings with member", + "name": "get resource booking with parameters 2 fromDb", "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", + " _.each(['id','projectId','status','startDate','endDate','billingAccountId','userId','jobId','rateType','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt','workPeriods'], field => {\r", + " pm.expect(response).to.have.property(field)\r", + " })\r", + " _.each(['id','projectId','paymentStatus','startDate','endDate','userHandle','resourceBookingId','daysWorked','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt'], field => {\r", + " pm.expect(response.workPeriods[0]).to.have.property(field)\r", + " })\r", + "});" ], "type": "text/javascript" } @@ -8316,51 +8294,26 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_member}}" + "value": "Bearer {{token_bookingManager}}" } ], "url": { - "raw": "{{URL}}/resourceBookings?sortOrder=desc", + "raw": "{{URL}}/resourceBookings/{{resourceBookingId}}?fields=workPeriods,id,projectId,status,startDate,endDate,billingAccountId,userId,jobId,rateType,memberRate,customerRate,createdBy,updatedBy,createdAt,updatedAt,deletedAt&fromDb=true", "host": [ "{{URL}}" ], "path": [ - "resourceBookings" + "resourceBookings", + "{{resourceBookingId}}" ], "query": [ { - "key": "page", - "value": "4", - "disabled": true - }, - { - "key": "perPage", - "value": "3", - "disabled": true - }, - { - "key": "sortBy", - "value": "id", - "disabled": true - }, - { - "key": "startDate", - "value": "2020-09-27", - "disabled": true - }, - { - "key": "endDate", - "value": "2020-09-27", - "disabled": true - }, - { - "key": "rateType", - "value": "hourly", - "disabled": true + "key": "fields", + "value": "workPeriods,id,projectId,status,startDate,endDate,billingAccountId,userId,jobId,rateType,memberRate,customerRate,createdBy,updatedBy,createdAt,updatedAt,deletedAt" }, { - "key": "sortOrder", - "value": "desc" + "key": "fromDb", + "value": "true" } ] } @@ -8368,16 +8321,21 @@ "response": [] }, { - "name": "search resource bookings with invalid token", + "name": "get resource booking with parameters 3", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 401', function () {\r", - " pm.response.to.have.status(401);\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", + " _.each(['id','projectId','status','startDate','endDate'], field => {\r", + " pm.expect(response).to.have.property(field)\r", + " })\r", + " _.each(['id','projectId','startDate','endDate','daysWorked'], field => {\r", + " pm.expect(response.workPeriods[0]).to.have.property(field)\r", + " })\r", "});" ], "type": "text/javascript" @@ -8390,51 +8348,1231 @@ { "key": "Authorization", "type": "text", - "value": "Bearer invalid_token" + "value": "Bearer {{token_bookingManager}}" } ], "url": { - "raw": "{{URL}}/resourceBookings?sortOrder=desc", + "raw": "{{URL}}/resourceBookings/{{resourceBookingId}}?fields=workPeriods.id,workPeriods.projectId,workPeriods.startDate,workPeriods.endDate,workPeriods.daysWorked,id,projectId,status,startDate,endDate", "host": [ "{{URL}}" ], "path": [ - "resourceBookings" + "resourceBookings", + "{{resourceBookingId}}" ], "query": [ { - "key": "page", - "value": "4", - "disabled": true - }, - { - "key": "perPage", - "value": "3", + "key": "fields", + "value": "workPeriods.id,workPeriods.projectId,workPeriods.startDate,workPeriods.endDate,workPeriods.daysWorked,id,projectId,status,startDate,endDate" + } + ] + } + }, + "response": [] + }, + { + "name": "get resource booking with parameters 3 fromDb", + "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", + " _.each(['id','projectId','status','startDate','endDate'], field => {\r", + " pm.expect(response).to.have.property(field)\r", + " })\r", + " _.each(['id','projectId','startDate','endDate','daysWorked'], field => {\r", + " pm.expect(response.workPeriods[0]).to.have.property(field)\r", + " })\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings/{{resourceBookingId}}?fields=workPeriods.id,workPeriods.projectId,workPeriods.startDate,workPeriods.endDate,workPeriods.daysWorked,id,projectId,status,startDate,endDate&fromDb=true", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings", + "{{resourceBookingId}}" + ], + "query": [ + { + "key": "fields", + "value": "workPeriods.id,workPeriods.projectId,workPeriods.startDate,workPeriods.endDate,workPeriods.daysWorked,id,projectId,status,startDate,endDate" + }, + { + "key": "fromDb", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "get resource booking with parameters 4", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You don't have access to view memberRate\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings/{{resourceBookingId}}?fields=memberRate", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings", + "{{resourceBookingId}}" + ], + "query": [ + { + "key": "fields", + "value": "memberRate" + } + ] + } + }, + "response": [] + }, + { + "name": "get resource booking with parameters 4 fromDb", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You don't have access to view memberRate\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings/{{resourceBookingId}}?fields=memberRate&fromDb=true", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings", + "{{resourceBookingId}}" + ], + "query": [ + { + "key": "fields", + "value": "memberRate" + }, + { + "key": "fromDb", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "get resource booking with parameters 5", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You don't have access to view workPeriods\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_all_resource_booking}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings/{{resourceBookingId}}?fields=workPeriods", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings", + "{{resourceBookingId}}" + ], + "query": [ + { + "key": "fields", + "value": "workPeriods" + } + ] + } + }, + "response": [] + }, + { + "name": "get resource booking with parameters 5 fromDb", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You don't have access to view workPeriods\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_all_resource_booking}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings/{{resourceBookingId}}?fields=workPeriods&fromDb=true", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings", + "{{resourceBookingId}}" + ], + "query": [ + { + "key": "fields", + "value": "workPeriods" + }, + { + "key": "fromDb", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with booking manager", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1", + "disabled": true + }, + { + "key": "perPage", + "value": "5", + "disabled": true + }, + { + "key": "sortBy", + "value": "id", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "startDate", + "value": "2020-09-27", + "disabled": true + }, + { + "key": "endDate", + "value": "2020-09-27", + "disabled": true + }, + { + "key": "rateType", + "value": "hourly", + "disabled": true + }, + { + "key": "status", + "value": "assigned", + "disabled": true + }, + { + "key": "projectIds", + "value": "111, 16705", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with m2m all", + "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", + "type": "text", + "value": "Bearer {{token_m2m_all_resource_booking}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1", + "disabled": true + }, + { + "key": "perPage", + "value": "5", + "disabled": true + }, + { + "key": "sortBy", + "value": "id", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "startDate", + "value": "2020-09-27", + "disabled": true + }, + { + "key": "endDate", + "value": "2020-09-27", + "disabled": true + }, + { + "key": "rateType", + "value": "hourly", + "disabled": true + }, + { + "key": "status", + "value": "assigned", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with connect user", + "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", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?sortOrder=desc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "4", + "disabled": true + }, + { + "key": "perPage", + "value": "3", + "disabled": true + }, + { + "key": "sortBy", + "value": "id", + "disabled": true + }, + { + "key": "startDate", + "value": "2020-09-27", + "disabled": true + }, + { + "key": "endDate", + "value": "2020-09-27", + "disabled": true + }, + { + "key": "rateType", + "value": "hourly", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?sortOrder=desc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "4", + "disabled": true + }, + { + "key": "perPage", + "value": "3", + "disabled": true + }, + { + "key": "sortBy", + "value": "id", + "disabled": true + }, + { + "key": "startDate", + "value": "2020-09-27", + "disabled": true + }, + { + "key": "endDate", + "value": "2020-09-27", + "disabled": true + }, + { + "key": "rateType", + "value": "hourly", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings 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": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer invalid_token" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?sortOrder=desc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "4", + "disabled": true + }, + { + "key": "perPage", + "value": "3", + "disabled": true + }, + { + "key": "sortBy", + "value": "id", + "disabled": true + }, + { + "key": "startDate", + "value": "2020-09-27", + "disabled": true + }, + { + "key": "endDate", + "value": "2020-09-27", + "disabled": true + }, + { + "key": "rateType", + "value": "hourly", "disabled": true }, { - "key": "sortBy", - "value": "id", - "disabled": true + "key": "sortOrder", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 1", + "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", + " _.each(['id','projectId','status','startDate','endDate','billingAccountId','userId','jobId','rateType','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt'], field => {\r", + " pm.expect(response[0]).to.have.property(field)\r", + " })\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?fields=id,projectId,status,startDate,endDate,billingAccountId,userId,jobId,rateType,memberRate,customerRate,createdBy,updatedBy,createdAt,updatedAt,deletedAt", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "id,projectId,status,startDate,endDate,billingAccountId,userId,jobId,rateType,memberRate,customerRate,createdBy,updatedBy,createdAt,updatedAt,deletedAt" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 2", + "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", + " _.each(['id','projectId','status','startDate','endDate','billingAccountId','userId','jobId','rateType','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt','workPeriods'], field => {\r", + " pm.expect(response[0]).to.have.property(field)\r", + " })\r", + " _.each(['id','projectId','paymentStatus','startDate','endDate','userHandle','resourceBookingId','daysWorked','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt'], field => {\r", + " pm.expect(response[0].workPeriods[0]).to.have.property(field)\r", + " })\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?fields=workPeriods,id,projectId,status,startDate,endDate,billingAccountId,userId,jobId,rateType,memberRate,customerRate,createdBy,updatedBy,createdAt,updatedAt,deletedAt", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "workPeriods,id,projectId,status,startDate,endDate,billingAccountId,userId,jobId,rateType,memberRate,customerRate,createdBy,updatedBy,createdAt,updatedAt,deletedAt" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 3", + "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", + " _.each(['id','projectId','status','startDate','endDate'], field => {\r", + " pm.expect(response[0]).to.have.property(field)\r", + " })\r", + " _.each(['id','projectId','startDate','endDate','daysWorked'], field => {\r", + " pm.expect(response[0].workPeriods[0]).to.have.property(field)\r", + " })\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?fields=workPeriods.id,workPeriods.projectId,workPeriods.startDate,workPeriods.endDate,workPeriods.daysWorked,id,projectId,status,startDate,endDate", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "workPeriods.id,workPeriods.projectId,workPeriods.startDate,workPeriods.endDate,workPeriods.daysWorked,id,projectId,status,startDate,endDate" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 4", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?fields=workPeriods.id,workPeriods.projectId,workPeriods.startDate,workPeriods.endDate,workPeriods.daysWorked,id,projectId,status,startDate,endDate,customerRate&workPeriods.startDate=2020-10-25&sortBy=customerRate&sortOrder=asc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "workPeriods.id,workPeriods.projectId,workPeriods.startDate,workPeriods.endDate,workPeriods.daysWorked,id,projectId,status,startDate,endDate,customerRate" + }, + { + "key": "workPeriods.startDate", + "value": "2020-10-25" + }, + { + "key": "sortBy", + "value": "customerRate" + }, + { + "key": "sortOrder", + "value": "asc" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 5", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?fields=workPeriods.id,workPeriods.projectId,workPeriods.startDate,workPeriods.endDate,workPeriods.daysWorked,id,projectId,status,startDate,endDate,customerRate,memberRate&workPeriods.startDate=2020-10-25&sortBy=workPeriods.daysWorked&sortOrder=desc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "workPeriods.id,workPeriods.projectId,workPeriods.startDate,workPeriods.endDate,workPeriods.daysWorked,id,projectId,status,startDate,endDate,customerRate,memberRate" + }, + { + "key": "workPeriods.startDate", + "value": "2020-10-25" + }, + { + "key": "sortBy", + "value": "workPeriods.daysWorked" + }, + { + "key": "sortOrder", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 6", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?fields=workPeriods.id,workPeriods.projectId,workPeriods.userHandle,workPeriods.startDate,workPeriods.endDate,workPeriods.daysWorked,id,projectId,status,startDate,endDate,customerRate,memberRate&projectId=111&workPeriods.startDate=2020-10-18&sortBy=workPeriods.userHandle&sortOrder=desc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "workPeriods.id,workPeriods.projectId,workPeriods.userHandle,workPeriods.startDate,workPeriods.endDate,workPeriods.daysWorked,id,projectId,status,startDate,endDate,customerRate,memberRate" + }, + { + "key": "projectId", + "value": "111" + }, + { + "key": "workPeriods.startDate", + "value": "2020-10-18" + }, + { + "key": "sortBy", + "value": "workPeriods.userHandle" + }, + { + "key": "sortOrder", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 7", + "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(\"Can not filter or sort by some field which is not included in fields\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?fields=id&projectId=111", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "id" }, { - "key": "startDate", - "value": "2020-09-27", - "disabled": true + "key": "projectId", + "value": "111" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 8", + "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(\"Can not filter or sort by some field which is not included in fields\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?fields=workPeriods&sortBy=customerRate", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "workPeriods" }, { - "key": "endDate", - "value": "2020-09-27", - "disabled": true + "key": "sortBy", + "value": "customerRate" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 9", + "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(\"Can not filter or sort by some field which is not included in fields\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?sortBy=workPeriods.paymentStatus", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "sortBy", + "value": "workPeriods.paymentStatus" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 10", + "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(\"Can not sort by workPeriod field without filtering by workPeriods.startDate or workPeriods.endDate\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?fields=workPeriods&sortBy=workPeriods.paymentStatus", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "workPeriods" }, { - "key": "rateType", - "value": "hourly", - "disabled": true + "key": "sortBy", + "value": "workPeriods.paymentStatus" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 11", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You don't have access to view memberRate\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?fields=memberRate,projectId&projectId=111", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "memberRate,projectId" }, { - "key": "sortOrder", - "value": "desc" + "key": "projectId", + "value": "111" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 12", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You don't have access to view workPeriods\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_all_resource_booking}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?fields=workPeriods", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "workPeriods" } ] } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 00dab590..c2de6ec2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1265,6 +1265,12 @@ paths: security: - bearerAuth: [] parameters: + - in: query + name: fields + description: the field names to be returned from both ResourceBooking and WorkPeriod + required: false + schema: + type: string - in: query name: page required: false @@ -1294,6 +1300,11 @@ paths: "rateType", "customerRate", "memberRate", + "workPeriods.userHandle", + "workPeriods.daysWorked", + "workPeriods.customerRate", + "workPeriods.memberRate", + "workPeriods.paymentStatus", ] description: The sort by column. - in: query @@ -1357,6 +1368,35 @@ paths: schema: type: string description: comma separated project ids. + - in: query + name: workPeriods.paymentStatus + required: false + schema: + type: string + enum: ["pending", "partially-completed", "completed", "cancelled"] + description: The payment status. + - in: query + name: workPeriods.startDate + required: false + schema: + type: string + format: date + pattern: '^\d{4}-\d{2}-\d{2}$' + description: The work period start date. + - in: query + name: workPeriods.endDate + required: false + schema: + type: string + format: date + pattern: '^\d{4}-\d{2}-\d{2}$' + description: The work period end date. + - in: query + name: workPeriods.userHandle + required: false + schema: + type: string + description: The user handle. responses: "200": @@ -1445,6 +1485,12 @@ paths: required: false schema: type: boolean + - in: query + name: fields + description: the field names to be returned from both ResourceBooking and WorkPeriod + required: false + schema: + type: string responses: "200": description: OK @@ -3397,7 +3443,18 @@ components: description: "The user id." status: type: string - enum: ["open", "placed", "selected", "client rejected - screening", "client rejected - interview", "rejected - other", "cancelled", "interview", "topcoder-rejected"] + enum: + [ + "open", + "placed", + "selected", + "client rejected - screening", + "client rejected - interview", + "rejected - other", + "cancelled", + "interview", + "topcoder-rejected", + ] description: "The job candidate status." default: open externalId: @@ -3479,7 +3536,15 @@ components: description: "Interview start time." status: type: string - enum: ["Scheduling", "Scheduled", "Requested for reschedule", "Rescheduled", "Completed", "Cancelled"] + enum: + [ + "Scheduling", + "Scheduled", + "Requested for reschedule", + "Rescheduled", + "Completed", + "Cancelled", + ] description: "The interview status." createdAt: type: string @@ -3521,7 +3586,15 @@ components: format: email status: type: string - enum: ["Scheduling", "Scheduled", "Requested for reschedule", "Rescheduled", "Completed", "Cancelled"] + enum: + [ + "Scheduling", + "Scheduled", + "Requested for reschedule", + "Rescheduled", + "Completed", + "Cancelled", + ] default: "Scheduling" description: "The interview status." UpdateInterviewRequestBody: @@ -3549,7 +3622,15 @@ components: format: date-time status: type: string - enum: ["Scheduling", "Scheduled", "Requested for reschedule", "Rescheduled", "Completed", "Cancelled"] + enum: + [ + "Scheduling", + "Scheduled", + "Requested for reschedule", + "Rescheduled", + "Completed", + "Cancelled", + ] description: "The interview status." JobPatchRequestBody: properties: @@ -3653,7 +3734,12 @@ components: billingAccountId: type: integer example: 80000071 - description: 'the billing account id for payments' + description: "the billing account id for payments" + workPeriods: + type: array + description: "The work periods related with resource booking" + items: + $ref: "#/components/schemas/WorkPeriod" createdAt: type: string format: date-time @@ -3721,7 +3807,7 @@ components: billingAccountId: type: integer example: 80000071 - description: 'the billing account id for payments' + description: "the billing account id for payments" ResourceBookingPatchRequestBody: properties: status: @@ -3754,7 +3840,7 @@ components: billingAccountId: type: integer example: 80000071 - description: 'the billing account id for payments' + description: "the billing account id for payments" WorkPeriod: required: - id @@ -3938,7 +4024,7 @@ components: billingAccountId: type: integer example: 80000071 - description: 'the billing account id for payments' + description: "the billing account id for payments" createdAt: type: string format: date-time diff --git a/package.json b/package.json index 00d15b73..0fa24cca 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "index:jobs": "node scripts/es/reIndexJobs.js", "index:job-candidates": "node scripts/es/reIndexJobCandidates.js", "index:resource-bookings": "node scripts/es/reIndexResourceBookings.js", - "index:work-periods": "node scripts/es/reIndexWorkPeriods.js", "data:export": "node scripts/data/exportData.js", "data:import": "node scripts/data/importData.js", "migrate": "npx sequelize db:migrate", @@ -86,4 +85,4 @@ "test/unit/**" ] } -} +} \ No newline at end of file diff --git a/scripts/data/exportData.js b/scripts/data/exportData.js index 356fcfc6..4eee1ad5 100644 --- a/scripts/data/exportData.js +++ b/scripts/data/exportData.js @@ -2,7 +2,7 @@ * Export data to a json file */ const config = require('config') -const { Interview, WorkPeriodPayment } = require('../../src/models') +const { Interview, WorkPeriod, WorkPeriodPayment } = require('../../src/models') const logger = require('../../src/common/logger') const helper = require('../../src/common/helper') @@ -14,17 +14,21 @@ const jobCandidateModelOpts = { }] } -const workPeriodModelOpts = { - modelName: 'WorkPeriod', +const resourceBookingModelOpts = { + modelName: 'ResourceBooking', include: [{ - model: WorkPeriodPayment, - as: 'payments' + model: WorkPeriod, + as: 'workPeriods', + include: [{ + model: WorkPeriodPayment, + as: 'payments' + }] }] } const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH const userPrompt = `WARNING: are you sure you want to export all data in the database to a json file with the path ${filePath}? This will overwrite the file.` -const dataModels = ['Job', jobCandidateModelOpts, 'ResourceBooking', workPeriodModelOpts] +const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts] async function exportData () { await helper.promptUser(userPrompt, async () => { diff --git a/scripts/data/importData.js b/scripts/data/importData.js index 52b3feb8..2e9c168e 100644 --- a/scripts/data/importData.js +++ b/scripts/data/importData.js @@ -2,7 +2,7 @@ * Import data from a json file into the db and index it in Elasticsearch */ const config = require('config') -const { Interview, WorkPeriodPayment } = require('../../src/models') +const { Interview, WorkPeriod, WorkPeriodPayment } = require('../../src/models') const logger = require('../../src/common/logger') const helper = require('../../src/common/helper') @@ -14,17 +14,21 @@ const jobCandidateModelOpts = { }] } -const workPeriodModelOpts = { - modelName: 'WorkPeriod', +const resourceBookingModelOpts = { + modelName: 'ResourceBooking', include: [{ - model: WorkPeriodPayment, - as: 'payments' + model: WorkPeriod, + as: 'workPeriods', + include: [{ + model: WorkPeriodPayment, + as: 'payments' + }] }] } const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH const userPrompt = `WARNING: this would remove existing data. Are you sure you want to import data from a json file with the path ${filePath}?` -const dataModels = ['Job', jobCandidateModelOpts, 'ResourceBooking', workPeriodModelOpts] +const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts] async function importData () { await helper.promptUser(userPrompt, async () => { diff --git a/scripts/es/createIndex.js b/scripts/es/createIndex.js index 4591214a..d2c72943 100644 --- a/scripts/es/createIndex.js +++ b/scripts/es/createIndex.js @@ -8,8 +8,7 @@ const helper = require('../../src/common/helper') const indices = [ config.get('esConfig.ES_INDEX_JOB'), config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), - config.get('esConfig.ES_INDEX_WORK_PERIOD') + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') ] const userPrompt = `WARNING: Are you sure want to create the following elasticsearch indices: ${indices}?` diff --git a/scripts/es/deleteIndex.js b/scripts/es/deleteIndex.js index 8acffe33..6e30995a 100644 --- a/scripts/es/deleteIndex.js +++ b/scripts/es/deleteIndex.js @@ -8,8 +8,7 @@ const helper = require('../../src/common/helper') const indices = [ config.get('esConfig.ES_INDEX_JOB'), config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), - config.get('esConfig.ES_INDEX_WORK_PERIOD') + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') ] const userPrompt = `WARNING: this would remove existent data! Are you sure want to delete the following eleasticsearch indices: ${indices}?` diff --git a/scripts/es/reIndexAll.js b/scripts/es/reIndexAll.js index 6a7bdf3f..802695dd 100644 --- a/scripts/es/reIndexAll.js +++ b/scripts/es/reIndexAll.js @@ -2,7 +2,7 @@ * Reindex all data in Elasticsearch using data from database */ const config = require('config') -const { Interview, WorkPeriodPayment } = require('../../src/models') +const { Interview, WorkPeriod, WorkPeriodPayment } = require('../../src/models') const logger = require('../../src/common/logger') const helper = require('../../src/common/helper') @@ -16,11 +16,15 @@ const jobCandidateModelOpts = { }] } -const workPeriodModelOpts = { - modelName: 'JobCandidate', +const resourceBookingModelOpts = { + modelName: 'ResourceBooking', include: [{ - model: WorkPeriodPayment, - as: 'payments' + model: WorkPeriod, + as: 'workPeriods', + include: [{ + model: WorkPeriodPayment, + as: 'payments' + }] }] } @@ -29,8 +33,7 @@ async function indexAll () { try { await helper.indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) await helper.indexBulkDataToES(jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger) - await helper.indexBulkDataToES('ResourceBooking', config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger) - await helper.indexBulkDataToES(workPeriodModelOpts, config.get('esConfig.ES_INDEX_WORK_PERIOD'), logger) + await helper.indexBulkDataToES(resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger) process.exit(0) } catch (err) { logger.logFullError(err, { component: 'indexAll' }) diff --git a/scripts/es/reIndexResourceBookings.js b/scripts/es/reIndexResourceBookings.js index ef2bf940..b29e3600 100644 --- a/scripts/es/reIndexResourceBookings.js +++ b/scripts/es/reIndexResourceBookings.js @@ -2,6 +2,7 @@ * Reindex ResourceBookings data in Elasticsearch using data from database */ const config = require('config') +const { WorkPeriod, WorkPeriodPayment } = require('../../src/models') const logger = require('../../src/common/logger') const helper = require('../../src/common/helper') @@ -10,11 +11,23 @@ const index = config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') const reIndexAllResourceBookingsPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the index ${index}` const reIndexResourceBookingPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the document with id ${resourceBookingId} in index ${index}?` +const resourceBookingModelOpts = { + modelName: 'ResourceBooking', + include: [{ + model: WorkPeriod, + as: 'workPeriods', + include: [{ + model: WorkPeriodPayment, + as: 'payments' + }] + }] +} + async function reIndexResourceBookings () { if (resourceBookingId === null) { await helper.promptUser(reIndexAllResourceBookingsPrompt, async () => { try { - await helper.indexBulkDataToES('ResourceBooking', index, logger) + await helper.indexBulkDataToES(resourceBookingModelOpts, index, logger) process.exit(0) } catch (err) { logger.logFullError(err, { component: 'reIndexResourceBookings' }) @@ -24,7 +37,7 @@ async function reIndexResourceBookings () { } else { await helper.promptUser(reIndexResourceBookingPrompt, async () => { try { - await helper.indexDataToEsById(resourceBookingId, 'ResourceBooking', index, logger) + await helper.indexDataToEsById(resourceBookingId, resourceBookingModelOpts, index, logger) process.exit(0) } catch (err) { logger.logFullError(err, { component: 'reIndexResourceBookings' }) diff --git a/scripts/es/reIndexWorkPeriods.js b/scripts/es/reIndexWorkPeriods.js deleted file mode 100644 index a6f30737..00000000 --- a/scripts/es/reIndexWorkPeriods.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Reindex WorkPeriods data in Elasticsearch using data from database - */ -const config = require('config') -const { WorkPeriodPayment } = require('../../src/models') -const logger = require('../../src/common/logger') -const helper = require('../../src/common/helper') - -const workPeriodId = helper.getParamFromCliArgs() -const index = config.get('esConfig.ES_INDEX_WORK_PERIOD') -const reIndexAllWorkPeriodsPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the index ${index}` -const reIndexWorkPeriodPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the document with id ${workPeriodId} in index ${index}?` - -const workPeriodModelOpts = { - modelName: 'WorkPeriod', - include: [{ - model: WorkPeriodPayment, - as: 'payments' - }] -} - -async function reIndexWorkPeriods () { - if (workPeriodId === null) { - await helper.promptUser(reIndexAllWorkPeriodsPrompt, async () => { - try { - await helper.indexBulkDataToES(workPeriodModelOpts, index, logger) - process.exit(0) - } catch (err) { - logger.logFullError(err, { component: 'reIndexWorkPeriods' }) - process.exit(1) - } - }) - } else { - await helper.promptUser(reIndexWorkPeriodPrompt, async () => { - try { - await helper.indexDataToEsById(workPeriodId, workPeriodModelOpts, index, logger) - process.exit(0) - } catch (err) { - logger.logFullError(err, { component: 'reIndexWorkPeriods' }) - process.exit(1) - } - }) - } -} - -reIndexWorkPeriods() diff --git a/src/common/helper.js b/src/common/helper.js index 26caf51b..ccdacc83 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -118,31 +118,35 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { customerRate: { type: 'float' }, rateType: { type: 'keyword' }, billingAccountId: { type: 'integer' }, - createdAt: { type: 'date' }, - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } -} -esIndexPropertyMapping[config.get('esConfig.ES_INDEX_WORK_PERIOD')] = { - resourceBookingId: { type: 'keyword' }, - userHandle: { type: 'keyword' }, - projectId: { type: 'integer' }, - userId: { type: 'keyword' }, - startDate: { type: 'date', format: 'yyyy-MM-dd' }, - endDate: { type: 'date', format: 'yyyy-MM-dd' }, - daysWorked: { type: 'integer' }, - memberRate: { type: 'float' }, - customerRate: { type: 'float' }, - paymentStatus: { type: 'keyword' }, - payments: { + workPeriods: { type: 'nested', properties: { id: { type: 'keyword' }, - workPeriodId: { type: 'keyword' }, - challengeId: { type: 'keyword' }, - amount: { type: 'float' }, - status: { type: 'keyword' }, - billingAccountId: { type: 'integer' }, + resourceBookingId: { type: 'keyword' }, + userHandle: { type: 'keyword' }, + projectId: { type: 'integer' }, + userId: { type: 'keyword' }, + startDate: { type: 'date', format: 'yyyy-MM-dd' }, + endDate: { type: 'date', format: 'yyyy-MM-dd' }, + daysWorked: { type: 'integer' }, + memberRate: { type: 'float' }, + customerRate: { type: 'float' }, + paymentStatus: { type: 'keyword' }, + payments: { + type: 'nested', + properties: { + id: { type: 'keyword' }, + workPeriodId: { type: 'keyword' }, + challengeId: { type: 'keyword' }, + amount: { type: 'float' }, + status: { type: 'keyword' }, + billingAccountId: { type: 'integer' }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, + updatedBy: { type: 'keyword' } + } + }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, @@ -419,17 +423,20 @@ async function importData (pathToFile, dataModels, logger) { as: 'interviews' }] } - const workPeriodModelOpts = { - modelName: 'WorkPeriod', + const resourceBookingModelOpts = { + modelName: 'ResourceBooking', include: [{ - model: models.WorkPeriodPayment, - as: 'payments' + model: models.WorkPeriod, + as: 'workPeriods', + include: [{ + model: models.WorkPeriodPayment, + as: 'payments' + }] }] } await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) await indexBulkDataToES(jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger) - await indexBulkDataToES('ResourceBooking', config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger) - await indexBulkDataToES(workPeriodModelOpts, config.get('esConfig.ES_INDEX_WORK_PERIOD'), logger) + await indexBulkDataToES(resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger) } /** @@ -1369,17 +1376,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 } diff --git a/src/controllers/ResourceBookingController.js b/src/controllers/ResourceBookingController.js index 098fd8e5..f8d3d566 100644 --- a/src/controllers/ResourceBookingController.js +++ b/src/controllers/ResourceBookingController.js @@ -11,7 +11,7 @@ const helper = require('../common/helper') * @param res the response */ async function getResourceBooking (req, res) { - res.send(await service.getResourceBooking(req.authUser, req.params.id, req.query.fromDb)) + res.send(await service.getResourceBooking(req.authUser, req.params.id, req.query)) } /** diff --git a/src/eventHandlers/ResourceBookingEventHandler.js b/src/eventHandlers/ResourceBookingEventHandler.js index 7a4a7c0a..3e8c18c3 100644 --- a/src/eventHandlers/ResourceBookingEventHandler.js +++ b/src/eventHandlers/ResourceBookingEventHandler.js @@ -155,7 +155,7 @@ async function createWorkPeriods (payload) { /** * When a ResourceBooking is updated, workPeriods related to * that ResourceBooking should be updated also. - * This function finds aout which workPeriods should be deleted, + * This function finds out which workPeriods should be deleted, * which ones should be created and which ones should be updated * @param {object} payload the event payload * @returns {undefined} diff --git a/src/models/ResourceBooking.js b/src/models/ResourceBooking.js index 58dc5454..54a95b89 100644 --- a/src/models/ResourceBooking.js +++ b/src/models/ResourceBooking.js @@ -1,5 +1,6 @@ const { Sequelize, Model } = require('sequelize') const config = require('config') +const _ = require('lodash') const errors = require('../common/errors') module.exports = (sequelize) => { @@ -9,8 +10,9 @@ module.exports = (sequelize) => { * @param {Object} models the database models */ static associate (models) { + ResourceBooking._models = models ResourceBooking.belongsTo(models.Job, { foreignKey: 'jobId' }) - ResourceBooking.hasMany(models.WorkPeriod, { foreignKey: 'resourceBookingId' }) + ResourceBooking.hasMany(models.WorkPeriod, { as: 'workPeriods', foreignKey: 'resourceBookingId' }) } /** @@ -18,12 +20,36 @@ module.exports = (sequelize) => { * @param {String} id the resource booking id * @returns {ResourceBooking} the resource booking instance */ - static async findById (id) { - const resourceBooking = await ResourceBooking.findOne({ + static async findById (id, options) { + const criteria = { where: { id } - }) + } + if (!_.isUndefined(options)) { + // Select ResourceBooking fields + if (options.include && options.include.length > 0) { + criteria.attributes = options.fieldsRB + } else if (options.excludeRB && options.excludeRB.length > 0) { + criteria.attributes = { exclude: options.excludeRB } + } + // include WorkPeriod model + if (options.withWorkPeriods) { + criteria.include = [{ + model: ResourceBooking._models.WorkPeriod, + as: 'workPeriods', + required: false + }] + // Select WorkPeriod fields + if (!options.allWorkPeriods) { + criteria.include[0].attributes = _.map(options.fieldsWP, f => _.split(f, '.')[1]) + } else if (options.excludeWP && options.excludeWP.length > 0) { + criteria.include[0].attributes = { exclude: _.map(options.excludeWP, f => _.split(f, '.')[1]) } + } + } + } + + const resourceBooking = await ResourceBooking.findOne(criteria) if (!resourceBooking) { throw new errors.NotFoundError(`id: ${id} "ResourceBooking" doesn't exists.`) } diff --git a/src/models/WorkPeriod.js b/src/models/WorkPeriod.js index 455818b5..75ff0597 100644 --- a/src/models/WorkPeriod.js +++ b/src/models/WorkPeriod.js @@ -20,12 +20,15 @@ module.exports = (sequelize) => { * @param {Object} options { withPayments: true/false } whether contains payments * @returns {WorkPeriod} the work period instance */ - static async findById (id, options = { withPayments: false }) { + static async findById (id, options = { withPayments: false, exclude: [] }) { const criteria = { where: { id } } + if (options.exclude && options.exclude.length > 0) { + criteria.attributes = { exclude: options.exclude } + } if (options.withPayments) { criteria.include = [{ model: WorkPeriod._models.WorkPeriodPayment, diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 3693f22c..81c21fda 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -12,23 +12,138 @@ const helper = require('../common/helper') const logger = require('../common/logger') const errors = require('../common/errors') const models = require('../models') +const constants = require('../../app-constants') const moment = require('moment') const ResourceBooking = models.ResourceBooking const WorkPeriod = models.WorkPeriod const esClient = helper.getESClient() +const cachedModelFields = _cacheModelFields() /** - * filter fields of resource booking by user role. + * Get the fields of the ResourceBooking model and the nested WorkPeriod model + * @returns {Array} array of field names + */ +function _cacheModelFields () { + const resourceBookingFields = _.keys(ResourceBooking.rawAttributes) + const workPeriodFields = _.map(_.keys(WorkPeriod.rawAttributes), key => `workPeriods.${key}`) + return [...resourceBookingFields, 'workPeriods', ...workPeriodFields] +} + +/** + * Check user scopes for getting workPeriods * @param {Object} currentUser the user who perform this operation. - * @param {Object} resourceBooking the resourceBooking with all fields - * @returns {Object} the resourceBooking + * @returns {Boolean} true if user is machine and has read/all workPeriod scopes */ -async function _getResourceBookingFilteringFields (currentUser, resourceBooking) { - if (currentUser.hasManagePermission || currentUser.isMachine) { - return resourceBooking +function _checkUserScopesForGetWorkPeriods (currentUser) { + const getWorkPeriodsScopes = [constants.Scopes.READ_WORK_PERIOD, constants.Scopes.ALL_WORK_PERIOD] + return currentUser.isMachine && helper.checkIfExists(getWorkPeriodsScopes, currentUser.scopes) +} + +/** + * Evaluates the criterias and returns the fields + * to be returned as a result of GET endpoints + * @param {Object} currentUser the user who perform this operation. + * @param {Object} criteria the query criterias + * @returns {Object} result + * @returns {Array} result.include field names to include + * @returns {Array} result.fieldsRB ResourceBooking field names to include + * @returns {Array} result.fieldsWP WorkPeriod field names to include + * @returns {Array} result.excludeRB ResourceBooking field names to exclude + * @returns {Array} result.excludeWP WorkPeriod field names to exclude + * @returns {Boolean} result.regularUser is current user a regular user? + * @returns {Boolean} result.allWorkPeriods will all WorkPeriod fields be returned? + * @returns {Boolean} result.withWorkPeriods does fields include any WorkPeriod field? + * @returns {Boolean} result.sortByWP will the sorting be done by WorkPeriod field? + */ +function _checkCriteriaAndGetFields (currentUser, criteria) { + const result = { + include: [], + fieldsRB: [], + fieldsWP: [], + excludeRB: [], + excludeWP: [] } - return _.omit(resourceBooking, 'memberRate') + const fields = criteria.fields + const sort = criteria.sortBy + const onlyResourceBooking = _.isUndefined(fields) + const query = onlyResourceBooking ? [] : _.split(fields, ',') + const notAllowedFields = _.difference(query, cachedModelFields) + // Check if fields criteria has a field name that RB or WP models don't have + if (notAllowedFields.length > 0) { + throw new errors.BadRequestError(`${notAllowedFields} are not allowed`) + } + // Check if user is a regular user. Regular users can't get ResourceBookings for which they are not a member + result.regularUser = !currentUser.hasManagePermission && !currentUser.isMachine && !currentUser.isConnectManager + // Check if all WorkPeriod fields will be returned + result.allWorkPeriods = _.some(query, q => q === 'workPeriods') + // Split the fields criteria into ResourceBooking and WorkPeriod fields + _.forEach(query, q => { + if (_.includes(q, '.')) { result.fieldsWP.push(q) } else if (q !== 'workPeriods') { result.fieldsRB.push(q) } + }) + // Check if any WorkPeriod field will be returned + result.withWorkPeriods = result.allWorkPeriods || result.fieldsWP.length > 0 + // Extract the filters from criteria parameter + let filters = _.filter(Object.keys(criteria), key => _.indexOf(['fromDb', 'fields', 'page', 'perPage', 'sortBy', 'sortOrder'], key) === -1) + filters = _.map(filters, f => { + if (f === 'projectIds') { + return 'projectId' + } return f + }) + const filterRB = [] + const filterWP = [] + // Split the filters criteria into ResourceBooking and WorkPeriod filters + _.forEach(filters, q => { if (_.includes(q, '.')) { filterWP.push(q) } else { filterRB.push(q) } }) + // Check if filter criteria has any WorkPeriod filter + const filterHasWorkPeriods = filterWP.length > 0 + // Check if sorting will be done by WorkPeriod field + result.sortByWP = _.split(sort, '.')[0] === 'workPeriods' + // Check if the current user has the right to see the memberRate + const canSeeMemberRate = currentUser.hasManagePermission || currentUser.isMachine + // If current user has no right to see the memberRate then it's excluded + // "currentUser.isMachine" to be true is not enough to return "workPeriods.memberRate" + // but returning "workPeriod" will be evaluated later + if (!canSeeMemberRate) { + result.excludeRB.push('memberRate') + result.excludeWP.push('workPeriods.memberRate') + } + // if "fields" is not included in cretia, then only ResourceBooking model will be returned + // No further evaluation is required as long as the criteria does not include a WorkPeriod filter or a WorkPeriod sorting condition + if (onlyResourceBooking) { + if (filterHasWorkPeriods || result.sortByWP) { + throw new errors.BadRequestError('Can not filter or sort by some field which is not included in fields') + } + result.excludeWP.push('workPeriods') + return result + } + // Include sorting condition in filters + if (result.sortByWP) { + // It is required to filter by "workPeriods.startDate" or "workPeriods.endDate" if sorting will be done by WorkPeriod field + if (!_.some(filterWP, f => _.includes(['workPeriods.startDate', 'workPeriods.endDate'], f))) { + throw new errors.BadRequestError('Can not sort by workPeriod field without filtering by workPeriods.startDate or workPeriods.endDate') + } + filterWP.push(sort) + } else if (!_.isUndefined(sort) && sort !== 'id') { + filterRB.push(sort) + } + // Check If it's tried to filter or sort by some field which should not be included as per rules of fields param + if (_.difference(filterRB, result.fieldsRB).length > 0) { + throw new errors.BadRequestError('Can not filter or sort by some field which is not included in fields') + } + // Check If it's tried to filter or sort by some field which should not be included as per rules of fields param + if (!result.allWorkPeriods && _.difference(filterWP, result.fieldsWP).length > 0) { + throw new errors.BadRequestError('Can not filter or sort by some field which is not included in fields') + } + // Check if the current user has no right to see the memberRate and memberRate is included in fields parameter + if (!canSeeMemberRate && _.some(query, q => _.includes(['memberRate', 'workPeriods.memberRate'], q))) { + throw new errors.ForbiddenError('You don\'t have access to view memberRate') + } + // Check if the current user has no right to see the workPeriods and workPeriods is included in fields parameter + if (currentUser.isMachine && result.withWorkPeriods && !_checkUserScopesForGetWorkPeriods(currentUser)) { + throw new errors.ForbiddenError('You don\'t have access to view workPeriods') + } + result.include.push(...query) + return result } /** @@ -39,9 +154,7 @@ async function _getResourceBookingFilteringFields (currentUser, resourceBooking) * @returns {undefined} */ async function _checkUserPermissionForGetResourceBooking (currentUser, projectId) { - if (!currentUser.hasManagePermission && !currentUser.isMachine && !currentUser.isConnectManager) { - await helper.checkIsMemberOfProject(currentUser.userId, projectId) - } + await helper.checkIsMemberOfProject(currentUser.userId, projectId) } /** @@ -92,43 +205,53 @@ async function _ensurePaidWorkPeriodsNotDeleted (resourceBookingId, oldValue, ne * Get resourceBooking by id * @param {Object} currentUser the user who perform this operation. * @param {String} id the resourceBooking id - * @param {Boolean} fromDb flag if query db for data or not + * @param {Object} criteria object including fields and fromDb criteria * @returns {Object} the resourceBooking */ -async function getResourceBooking (currentUser, id, fromDb = false) { - if (!fromDb) { +async function getResourceBooking (currentUser, id, criteria) { + // Evaluate criteria and extract the fields to be included or excluded + const queryOpt = _checkCriteriaAndGetFields(currentUser, criteria) + // We don't allow regular user to exclude projectId from result + if (queryOpt.regularUser && queryOpt.include.length > 0 && !_.includes(queryOpt.include, 'projectId')) { + throw new errors.ForbiddenError('Not allowed without including "projectId"') + } + if (!criteria.fromDb) { try { const resourceBooking = await esClient.get({ index: config.esConfig.ES_INDEX_RESOURCE_BOOKING, - id + id, + _source_includes: [...queryOpt.include], + _source_excludes: ['workPeriods.payments', ...queryOpt.excludeRB, ...queryOpt.excludeWP] }) - - await _checkUserPermissionForGetResourceBooking(currentUser, resourceBooking.body._source.projectId) // check user permission - - const resourceBookingRecord = { id: resourceBooking.body._id, ...resourceBooking.body._source } - return _getResourceBookingFilteringFields(currentUser, resourceBookingRecord) + if (queryOpt.regularUser) { + await _checkUserPermissionForGetResourceBooking(currentUser, resourceBooking.body._source.projectId) // check user permission + } + return resourceBooking.body._source } catch (err) { if (helper.isDocumentMissingException(err)) { throw new errors.NotFoundError(`id: ${id} "ResourceBooking" not found`) } - if (err.httpStatus === HttpStatus.FORBIDDEN) { + if (err.httpStatus === HttpStatus.UNAUTHORIZED) { throw err } logger.logFullError(err, { component: 'ResourceBookingService', context: 'getResourceBooking' }) } } logger.info({ component: 'ResourceBookingService', context: 'getResourceBooking', message: 'try to query db for data' }) - const resourceBooking = await ResourceBooking.findById(id) - - await _checkUserPermissionForGetResourceBooking(currentUser, resourceBooking.projectId) // check user permission - - return _getResourceBookingFilteringFields(currentUser, resourceBooking.dataValues) + const resourceBooking = await ResourceBooking.findById(id, queryOpt) + if (queryOpt.regularUser) { + await _checkUserPermissionForGetResourceBooking(currentUser, resourceBooking.projectId) // check user permission + } + return resourceBooking.dataValues } getResourceBooking.schema = Joi.object().keys({ currentUser: Joi.object().required(), id: Joi.string().guid().required(), - fromDb: Joi.boolean() + criteria: Joi.object().keys({ + fromDb: Joi.boolean().default(false), + fields: Joi.string() + }) }).required() /** @@ -306,12 +429,14 @@ deleteResourceBooking.schema = Joi.object().keys({ * @returns {Object} the search result, contain total/page/perPage and result array */ async function searchResourceBookings (currentUser, criteria, options = { returnAll: false }) { + // Evaluate criteria and extract the fields to be included or excluded + const queryOpt = _checkCriteriaAndGetFields(currentUser, criteria) // check user permission - if (!currentUser.hasManagePermission && !currentUser.isMachine && !currentUser.isConnectManager && !options.returnAll) { + if (queryOpt.regularUser && !options.returnAll) { if (!criteria.projectId) { // regular user can only search with filtering by "projectId" throw new errors.ForbiddenError('Not allowed without filtering by "projectId"') } - await helper.checkIsMemberOfProject(currentUser.userId, criteria.projectId) + await _checkUserPermissionForGetResourceBooking(currentUser, criteria.projectId) } // `criteria`.projectIds` could be array of ids, or comma separated string of ids @@ -326,7 +451,7 @@ async function searchResourceBookings (currentUser, criteria, options = { return return projectId }) } - const page = criteria.page > 0 ? criteria.page : 1 + const page = criteria.page let perPage if (options.returnAll) { // To simplify the logic we are use a very large number for perPage @@ -335,7 +460,7 @@ async function searchResourceBookings (currentUser, criteria, options = { return // https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#index-max-result-window perPage = 10000 } else { - perPage = criteria.perPage > 0 ? criteria.perPage : 20 + perPage = criteria.perPage } if (!criteria.sortBy) { @@ -345,21 +470,30 @@ async function searchResourceBookings (currentUser, criteria, options = { return criteria.sortOrder = 'desc' } try { - const sort = [{ [criteria.sortBy === 'id' ? '_id' : criteria.sortBy]: { order: criteria.sortOrder } }] - const esQuery = { index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + _source_includes: queryOpt.include, + _source_excludes: ['workPeriods.payments', ...queryOpt.excludeRB, ...queryOpt.excludeWP], body: { query: { bool: { - must: [] + must: [ + { + nested: { + path: 'workPeriods', + query: { bool: { must: [] } } + } + } + ] } }, from: (page - 1) * perPage, - size: perPage, - sort + size: perPage } } + if (!queryOpt.sortByWP) { + esQuery.body.sort = [{ [criteria.sortBy === 'id' ? '_id' : criteria.sortBy]: { order: criteria.sortOrder } }] + } // change the date format to match with index schema if (criteria.startDate) { criteria.startDate = moment(criteria.startDate).format('YYYY-MM-DD') @@ -367,6 +501,13 @@ async function searchResourceBookings (currentUser, criteria, options = { return if (criteria.endDate) { criteria.endDate = moment(criteria.endDate).format('YYYY-MM-DD') } + if (criteria['workPeriods.startDate']) { + criteria['workPeriods.startDate'] = moment(criteria['workPeriods.startDate']).format('YYYY-MM-DD') + } + if (criteria['workPeriods.endDate']) { + criteria['workPeriods.endDate'] = moment(criteria['workPeriods.endDate']).format('YYYY-MM-DD') + } + // Apply ResourceBooking filters _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId']), (value, key) => { esQuery.body.query.bool.must.push({ term: { @@ -384,52 +525,110 @@ async function searchResourceBookings (currentUser, criteria, options = { return } }] } + // Apply WorkPeriod filters + _.each(_.pick(criteria, ['workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle']), (value, key) => { + esQuery.body.query.bool.must[0].nested.query.bool.must.push({ + term: { + [key]: { + value + } + } + }) + }) logger.debug({ component: 'ResourceBookingService', context: 'searchResourceBookings', message: `Query: ${JSON.stringify(esQuery)}` }) const { body } = await esClient.search(esQuery) - + let resourceBookings = _.map(body.hits.hits, '_source') + // ESClient will return ResourceBookings with it's all nested WorkPeriods + // We re-apply WorkPeriod filters + _.each(_.pick(criteria, ['workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle', 'workPeriods.paymentStatus']), (value, key) => { + key = key.split('.')[1] + _.each(resourceBookings, r => { + r.workPeriods = _.filter(r.workPeriods, { [key]: value }) + }) + }) + // If sorting criteria is WorkPeriod field, we have to sort manually + if (queryOpt.sortByWP) { + const sorts = criteria.sortBy.split('.') + resourceBookings = _.sortBy(resourceBookings, [`${sorts[0]}[0].${sorts[1]}`]) + if (criteria.sortOrder === 'desc') { + resourceBookings = _.reverse(resourceBookings) + } + } return { total: body.hits.total.value, page, perPage, - result: _.map(body.hits.hits, (hit) => { - const obj = _.cloneDeep(hit._source) - obj.id = hit._id - return obj - }) + result: resourceBookings } } catch (err) { logger.logFullError(err, { component: 'ResourceBookingService', context: 'searchResourceBookings' }) } logger.info({ component: 'ResourceBookingService', context: 'searchResourceBookings', message: 'fallback to DB query' }) const filter = { [Op.and]: [] } + // Apply ResourceBooking filters _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId']), (value, key) => { filter[Op.and].push({ [key]: value }) }) if (criteria.projectIds) { filter[Op.and].push({ projectId: criteria.projectIds }) } - const resourceBookings = await ResourceBooking.findAll({ + const queryCriteria = { where: filter, offset: ((page - 1) * perPage), - limit: perPage, - order: [[criteria.sortBy, criteria.sortOrder]] - }) + limit: perPage + } + // Select ResourceBooking fields + if (queryOpt.include.length > 0) { + queryCriteria.attributes = queryOpt.fieldsRB + } else if (queryOpt.excludeRB && queryOpt.excludeRB.length > 0) { + queryCriteria.attributes = { exclude: queryOpt.excludeRB } + } + // Include WorkPeriod Model + if (queryOpt.withWorkPeriods) { + queryCriteria.include = [{ + model: WorkPeriod, + as: 'workPeriods', + required: false, + where: { [Op.and]: [] } + }] + // Select WorkPeriod fields + if (!queryOpt.allWorkPeriods) { + queryCriteria.include[0].attributes = _.map(queryOpt.fieldsWP, f => _.split(f, '.')[1]) + } else if (queryOpt.excludeWP && queryOpt.excludeWP.length > 0) { + queryCriteria.include[0].attributes = { exclude: _.map(queryOpt.excludeWP, f => _.split(f, '.')[1]) } + } + // Apply WorkPeriod filters + _.each(_.pick(criteria, ['workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle', 'workPeriods.paymentStatus']), (value, key) => { + key = key.split('.')[1] + queryCriteria.include[0].where[Op.and].push({ [key]: value }) + queryCriteria.include[0].required = true + }) + } + // Apply sorting criteria + if (!queryOpt.sortByWP) { + queryCriteria.order = [[criteria.sortBy, criteria.sortOrder]] + } else { + queryCriteria.order = [[{ model: WorkPeriod, as: 'workPeriods' }, _.split(criteria.sortBy, '.')[1], criteria.sortOrder]] + } + const resourceBookings = await ResourceBooking.findAll(queryCriteria) return { fromDb: true, total: resourceBookings.length, page, perPage, - result: _.map(resourceBookings, resourceBooking => resourceBooking.dataValues) + result: resourceBookings } } searchResourceBookings.schema = Joi.object().keys({ currentUser: Joi.object().required(), criteria: Joi.object().keys({ - page: Joi.number().integer(), - perPage: Joi.number().integer(), - sortBy: Joi.string().valid('id', 'rateType', 'startDate', 'endDate', 'customerRate', 'memberRate', 'status'), + fields: Joi.string(), + page: Joi.page(), + perPage: Joi.perPage(), + sortBy: Joi.string().valid('id', 'rateType', 'startDate', 'endDate', 'customerRate', 'memberRate', 'status', + 'workPeriods.userHandle', 'workPeriods.daysWorked', 'workPeriods.customerRate', 'workPeriods.memberRate', 'workPeriods.paymentStatus'), sortOrder: Joi.string().valid('desc', 'asc'), status: Joi.resourceBookingStatus(), startDate: Joi.date().format('YYYY-MM-DD'), @@ -441,7 +640,11 @@ searchResourceBookings.schema = Joi.object().keys({ projectIds: Joi.alternatives( Joi.string(), Joi.array().items(Joi.number().integer()) - ) + ), + 'workPeriods.paymentStatus': Joi.paymentStatus(), + 'workPeriods.startDate': Joi.date().format('YYYY-MM-DD'), + 'workPeriods.endDate': Joi.date().format('YYYY-MM-DD'), + 'workPeriods.userHandle': Joi.string() }).required(), options: Joi.object() }).required() diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 2f9b32c5..d3ff606e 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 (var key in subjectBody) { subjectBody[key] = await helper.substituteStringByObject(subjectBody[key], data.data) } const emailData = { diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index e8694c76..c196f88e 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -44,25 +44,36 @@ async function getWorkPeriodPayment (currentUser, id, fromDb = false) { await _checkUserPermissionForCRUWorkPeriodPayment(currentUser) if (!fromDb) { try { - const workPeriod = await esClient.search({ - index: config.esConfig.ES_INDEX_WORK_PERIOD, - _source: 'payments', + const resourceBooking = await esClient.search({ + index: config.esConfig.ES_INDEX_RESOURCE_BOOKING, + _source: 'workPeriods.payments', body: { query: { nested: { - path: 'payments', + path: 'workPeriods.payments', query: { - match: { 'payments.id': id } + match: { 'workPeriods.payments.id': id } } } } } }) - if (!workPeriod.body.hits.total.value) { + if (!resourceBooking.body.hits.total.value) { throw new errors.NotFoundError() } - const workPeriodPaymentRecord = _.find(workPeriod.body.hits.hits[0]._source.payments, { id }) + let workPeriodPaymentRecord = null + _.forEach(resourceBooking.body.hits.hits[0]._source.workPeriods, wp => { + _.forEach(wp.payments, p => { + if (p.id === id) { + workPeriodPaymentRecord = p + return false + } + }) + if (workPeriodPaymentRecord) { + return false + } + }) return workPeriodPaymentRecord } catch (err) { if (err.httpStatus === HttpStatus.NOT_FOUND) { @@ -244,12 +255,12 @@ async function searchWorkPeriodPayments (currentUser, criteria, options = { retu const perPage = criteria.perPage try { const esQuery = { - index: config.get('esConfig.ES_INDEX_WORK_PERIOD'), - _source: 'payments', + index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + _source: 'workPeriods.payments', body: { query: { nested: { - path: 'payments', + path: 'workPeriods.payments', query: { bool: { must: [] } } } }, @@ -263,7 +274,7 @@ async function searchWorkPeriodPayments (currentUser, criteria, options = { retu _.each(_.pick(criteria, ['status', 'workPeriodId']), (value, key) => { esQuery.body.query.nested.query.bool.must.push({ term: { - [`payments.${key}`]: { + [`workPeriods.payments.${key}`]: { value } } @@ -272,14 +283,20 @@ async function searchWorkPeriodPayments (currentUser, criteria, options = { retu if (criteria.workPeriodIds) { esQuery.body.query.nested.query.bool.filter = [{ terms: { - 'payments.workPeriodId': criteria.workPeriodIds + 'workPeriods.payments.workPeriodId': criteria.workPeriodIds } }] } logger.debug({ component: 'WorkPeriodPaymentService', context: 'searchWorkPeriodPayment', message: `Query: ${JSON.stringify(esQuery)}` }) const { body } = await esClient.search(esQuery) - let payments = _.reduce(body.hits.hits, (acc, workPeriod) => _.concat(acc, workPeriod._source.payments), []) + const workPeriods = _.reduce(body.hits.hits, (acc, resourceBooking) => _.concat(acc, resourceBooking._source.workPeriods), []) + let payments = _.reduce(workPeriods, (acc, workPeriod) => _.concat(acc, workPeriod.payments), []) + if (criteria.workPeriodId) { + payments = _.filter(payments, { workPeriodId: criteria.workPeriodId }) + } else if (criteria.workPeriodIds) { + payments = _.filter(payments, p => _.includes(criteria.workPeriodIds, p.workPeriodId)) + } if (criteria.status) { payments = _.filter(payments, { status: criteria.status }) } @@ -320,9 +337,7 @@ async function searchWorkPeriodPayments (currentUser, criteria, options = { retu total: workPeriodPayments.length, page, perPage, - result: _.map(workPeriodPayments, workPeriodPayment => { - return workPeriodPayment.dataValues - }) + result: workPeriodPayments } } diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index 92e1deb0..19346ff3 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -83,16 +83,27 @@ function _checkUserScopesForGetPayments (currentUser) { } /** - * filter fields of work period by user role. + * Get which fields to be excluded from result * @param {Object} currentUser the user who perform this operation. - * @param {Object} workPeriod the workPeriod with all fields - * @returns {Object} the workPeriod + * @returns {Object} queryOpt + * @returns {Object} queryOpt.excludeES excluded fields for ES query + * @returns {Object} queryOpt.excludeDB excluded fields for DB query + * @returns {Object} queryOpt.withPayments is payments field included? */ -async function _getWorkPeriodFilteringFields (currentUser, workPeriod) { - if (currentUser.hasManagePermission || _checkUserScopesForGetPayments(currentUser)) { - return workPeriod +function _getWorkPeriodFilteringFields (currentUser) { + const queryOpt = { + excludeES: [], + excludeDB: [], + withPayments: false } - return _.omit(workPeriod, ['memberRate', 'payments']) + if (!currentUser.hasManagePermission && !currentUser.isMachine) { + queryOpt.excludeES.push('workPeriods.memberRate') + queryOpt.excludeDB.push('memberRate') + } + if (currentUser.hasManagePermission || _checkUserScopesForGetPayments(currentUser)) { + queryOpt.withPayments = true + } else { queryOpt.excludeES.push('workPeriods.payments') } + return queryOpt } /** @@ -144,33 +155,45 @@ function _autoCalculateDates (data) { * @returns {Object} the workPeriod */ async function getWorkPeriod (currentUser, id, fromDb = false) { + // get query options according to currentUser + const queryOpt = _getWorkPeriodFilteringFields(currentUser) if (!fromDb) { try { - const workPeriod = await esClient.get({ - index: config.esConfig.ES_INDEX_WORK_PERIOD, - id + const resourceBooking = await esClient.search({ + index: config.esConfig.ES_INDEX_RESOURCE_BOOKING, + _source_includes: 'workPeriods', + _source_excludes: queryOpt.excludeES, + body: { + query: { + nested: { + path: 'workPeriods', + query: { + match: { 'workPeriods.id': id } + } + } + } + } }) - - await _checkUserPermissionForGetWorkPeriod(currentUser, workPeriod.body._source.projectId) // check user permission - - const workPeriodRecord = { id: workPeriod.body._id, ...workPeriod.body._source } - return _getWorkPeriodFilteringFields(currentUser, workPeriodRecord) + if (!resourceBooking.body.hits.total.value) { + throw new errors.NotFoundError() + } + await _checkUserPermissionForGetWorkPeriod(currentUser, resourceBooking.body.hits.hits[0]._source.workPeriods.projectId) // check user permission + return _.find(resourceBooking.body.hits.hits[0]._source.workPeriods, { id }) } catch (err) { if (helper.isDocumentMissingException(err)) { throw new errors.NotFoundError(`id: ${id} "WorkPeriod" not found`) } - if (err.httpStatus === HttpStatus.FORBIDDEN) { + if (err.httpStatus === HttpStatus.UNAUTHORIZED) { throw err } logger.logFullError(err, { component: 'WorkPeriodService', context: 'getWorkPeriod' }) } } logger.info({ component: 'WorkPeriodService', context: 'getWorkPeriod', message: 'try to query db for data' }) - const workPeriod = await WorkPeriod.findById(id, { withPayments: true }) + const workPeriod = await WorkPeriod.findById(id, { withPayments: queryOpt.withPayments, exclude: queryOpt.excludeDB }) await _checkUserPermissionForGetWorkPeriod(currentUser, workPeriod.projectId) // check user permission - // We should only return "memberRate" to Booking Manager, Administrator or M2M - return _getWorkPeriodFilteringFields(currentUser, workPeriod.dataValues) + return workPeriod.dataValues } getWorkPeriod.schema = Joi.object().keys({ @@ -364,7 +387,7 @@ async function searchWorkPeriods (currentUser, criteria, options = { returnAll: } await helper.checkIsMemberOfProject(currentUser.userId, criteria.projectId) } - + const queryOpt = _getWorkPeriodFilteringFields(currentUser) // `criteria.resourceBookingIds` could be array of ids, or comma separated string of ids // in case it's comma separated string of ids we have to convert it to an array of ids if ((typeof criteria.resourceBookingIds) === 'string') { @@ -377,17 +400,7 @@ async function searchWorkPeriods (currentUser, criteria, options = { returnAll: }) } const page = criteria.page - let perPage - if (options.returnAll) { - // To simplify the logic we are use a very large number for perPage - // because in practice there could hardly be so many records to be returned.(also consider we are using filters in the meantime) - // the number is limited by `index.max_result_window`, its default value is 10000, see - // https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#index-max-result-window - perPage = 10000 - } else { - perPage = criteria.perPage - } - + const perPage = criteria.perPage if (!criteria.sortBy) { criteria.sortBy = 'id' } @@ -395,19 +408,22 @@ async function searchWorkPeriods (currentUser, criteria, options = { returnAll: criteria.sortOrder = 'desc' } try { - const sort = [{ [criteria.sortBy === 'id' ? '_id' : criteria.sortBy]: { order: criteria.sortOrder } }] - const esQuery = { - index: config.get('esConfig.ES_INDEX_WORK_PERIOD'), + index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + _source_includes: 'workPeriods', + _source_excludes: queryOpt.excludeES, body: { query: { - bool: { - must: [] + nested: { + path: 'workPeriods', + query: { bool: { must: [] } } } }, - from: (page - 1) * perPage, - size: perPage, - sort + size: 10000 + // We use a very large number for size, because we can't paginate nested documents + // and in practice there could hardly be so many records to be returned.(also consider we are using filters in the meantime) + // the number is limited by `index.max_result_window`, its default value is 10000, see + // https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#index-max-result-window } } // change the date format to match with database model @@ -417,10 +433,11 @@ async function searchWorkPeriods (currentUser, criteria, options = { returnAll: if (criteria.endDate) { criteria.endDate = moment(criteria.endDate).format('YYYY-MM-DD') } + // Apply filters _.each(_.pick(criteria, ['resourceBookingId', 'userHandle', 'projectId', 'startDate', 'endDate', 'paymentStatus']), (value, key) => { - esQuery.body.query.bool.must.push({ + esQuery.body.query.nested.query.bool.must.push({ term: { - [key]: { + [`workPeriods.${key}`]: { value } } @@ -428,30 +445,34 @@ async function searchWorkPeriods (currentUser, criteria, options = { returnAll: }) // if criteria contains resourceBookingIds, filter resourceBookingId with this value if (criteria.resourceBookingIds) { - esQuery.body.query.bool.filter = [{ + esQuery.body.query.nested.query.bool.filter = [{ terms: { - resourceBookingId: criteria.resourceBookingIds + 'workPeriods.resourceBookingId': criteria.resourceBookingIds } }] } logger.debug({ component: 'WorkPeriodService', context: 'searchWorkPeriods', message: `Query: ${JSON.stringify(esQuery)}` }) const { body } = await esClient.search(esQuery) - + let workPeriods = _.reduce(body.hits.hits, (acc, resourceBooking) => _.concat(acc, resourceBooking._source.workPeriods), []) + // ESClient will return ResourceBookings with it's all nested WorkPeriods + // We re-apply WorkPeriod filters + _.each(_.pick(criteria, ['startDate', 'endDate', 'paymentStatus']), (value, key) => { + workPeriods = _.filter(workPeriods, { [key]: value }) + }) + workPeriods = _.sortBy(workPeriods, [criteria.sortBy]) + if (criteria.sortOrder === 'desc') { + workPeriods = _.reverse(workPeriods) + } + const total = workPeriods.length + if (!options.returnAll) { + workPeriods = _.slice(workPeriods, (page - 1) * perPage, page * perPage) + } return { - total: body.hits.total.value, + total, page, perPage, - result: _.map(body.hits.hits, (hit) => { - const obj = _.cloneDeep(hit._source) - obj.id = hit._id - // We should only return "memberRate" to Booking Manager, Administrator or M2M - if (!currentUser.hasManagePermission && !_checkUserScopesForGetPayments(currentUser)) { - delete obj.memberRate - delete obj.payments - } - return obj - }) + result: workPeriods } } catch (err) { logger.logFullError(err, { component: 'WorkPeriodService', context: 'searchWorkPeriods' }) @@ -464,30 +485,31 @@ async function searchWorkPeriods (currentUser, criteria, options = { returnAll: if (criteria.resourceBookingIds) { filter[Op.and].push({ resourceBookingId: criteria.resourceBookingIds }) } - const workPeriods = await WorkPeriod.findAll({ + const queryCriteria = { where: filter, - include: [{ - model: models.WorkPeriodPayment, - as: 'payments', - required: false - }], offset: ((page - 1) * perPage), limit: perPage, order: [[criteria.sortBy, criteria.sortOrder]] - }) + } + // add excluded fields criteria + if (queryOpt.excludeDB.length > 0) { + queryCriteria.attributes = { exclude: queryOpt.excludeDB } + } + // include WorkPeriodPayment model + if (queryOpt.withPayments) { + queryCriteria.include = [{ + model: models.WorkPeriodPayment, + as: 'payments', + required: false + }] + } + const workPeriods = await WorkPeriod.findAll(queryCriteria) return { fromDb: true, total: workPeriods.length, page, perPage, - result: _.map(workPeriods, workPeriod => { - // We should only return "memberRate" to Booking Manager, Administrator or M2M - if (!currentUser.hasManagePermission && !_checkUserScopesForGetPayments(currentUser)) { - delete workPeriod.dataValues.memberRate - delete workPeriod.dataValues.payments - } - return workPeriod.dataValues - }) + result: workPeriods } } diff --git a/test/prepare.js b/test/prepare.js index 42277c70..c7e16a81 100644 --- a/test/prepare.js +++ b/test/prepare.js @@ -1,7 +1,10 @@ /* * Prepare for tests. */ - +const sinon = require('sinon') +const helper = require('../src/common/helper') +const commonData = require('./unit/common/CommonData') process.env.NODE_ENV = 'test' +sinon.stub(helper, 'getESClient').callsFake(() => commonData.ESClient) require('../src/bootstrap') require('../src/eventHandlers').init() diff --git a/test/unit/ResourceBookingService.test.js b/test/unit/ResourceBookingService.test.js index f557466e..64de1900 100644 --- a/test/unit/ResourceBookingService.test.js +++ b/test/unit/ResourceBookingService.test.js @@ -8,6 +8,7 @@ const workPeriodService = require('../../src/services/WorkPeriodService') const commonData = require('./common/CommonData') const testData = require('./common/ResourceBookingData') const helper = require('../../src/common/helper') +const errors = require('../../src/common/errors') const busApiClient = helper.getBusApiClient() const ResourceBooking = models.ResourceBooking const WorkPeriod = models.WorkPeriod @@ -353,4 +354,225 @@ describe('resourceBooking service test', () => { expect(stubDeleteWorkPeriodService.callCount).to.eq(0) }) }) + describe('Get resource booking with/without nested fields', () => { + it('T17:Get resource booking from ES', async () => { + const data = testData.T17 + const ESClient = commonData.ESClient + ESClient.get = () => {} + const esClientGet = sinon.stub(ESClient, 'get').callsFake(() => data.esClientGet) + const result = await service.getResourceBooking(commonData.userWithManagePermission, data.esClientGet.body._source.id, data.criteria) + expect(esClientGet.calledOnce).to.be.true + expect(result).to.deep.eq(data.esClientGet.body._source) + }) + it('T18:Get resource booking from DB', async () => { + const data = testData.T18 + const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { + return data.resourceBooking.value + }) + const result = await service.getResourceBooking(commonData.userWithManagePermission, data.resourceBooking.value.dataValues.id, data.criteria) + expect(stubResourceBookingFindById.calledOnce).to.be.true + expect(result).to.deep.eq(data.resourceBooking.value.dataValues) + }) + it('T19:Fail to get resource booking with not allowed fields', async () => { + const data = testData.T19 + let error + try { + await service.getResourceBooking(commonData.userWithManagePermission, data.id, data.criteria) + } catch (err) { + error = err + } + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + it('T20:Fail to get resource booking with memberRate', async () => { + const data = testData.T20 + let error + try { + await service.getResourceBooking(commonData.regularUser, data.id, data.criteria) + } catch (err) { + error = err + } + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + it('T21:Fail to get resource booking with nested workPeriods', async () => { + const data = testData.T21 + let error + try { + await service.getResourceBooking(commonData.currentUser, data.id, data.criteria) + } catch (err) { + error = err + } + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + it('T22:Fail to get resource booking without including projectId as a regularUser', async () => { + const data = testData.T22 + let error + try { + await service.getResourceBooking(commonData.regularUser, data.id, data.criteria) + } catch (err) { + error = err + } + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + it('T23:Fail to get resource booking as a regularUser who is not member of project', async () => { + const data = testData.T23 + const ESClient = commonData.ESClient + ESClient.get = () => {} + const esClientGet = sinon.stub(ESClient, 'get').callsFake(() => data.esClientGet) + const checkIsMemberOfProject = sinon.stub(helper, 'checkIsMemberOfProject').callsFake(() => { + throw new errors.UnauthorizedError(data.error.message) + }) + let error + try { + await service.getResourceBooking(commonData.regularUser, data.id, data.criteria) + } catch (err) { + error = err + } + expect(esClientGet.calledOnce).to.be.true + expect(checkIsMemberOfProject.calledOnce).to.be.true + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + }) + describe('Search resource booking with/without nested fields', () => { + it('T24:Search resource booking from ES', async () => { + const data = testData.T24 + const ESClient = commonData.ESClient + ESClient.search = () => {} + const esClientSearch = sinon.stub(ESClient, 'search').callsFake(() => data.esClientSearch) + const result = await service.searchResourceBookings(commonData.userWithManagePermission, data.criteria) + expect(esClientSearch.calledOnce).to.be.true + expect(result).to.deep.eq(data.result) + }) + it('T25:Search resource bookin from DB', async () => { + const data = testData.T25 + const ESClient = commonData.ESClient + ESClient.search = () => {} + const esClientSearch = sinon.stub(ESClient, 'search').callsFake(() => { throw new Error() }) + const stubResourceBookingFindAll = sinon.stub(ResourceBooking, 'findAll').callsFake(async () => { + return data.resourceBookingFindAll + }) + const result = await service.searchResourceBookings(commonData.userWithManagePermission, data.criteria) + expect(esClientSearch.calledOnce).to.be.true + expect(stubResourceBookingFindAll.calledOnce).to.be.true + expect(result).to.deep.eq(data.result) + }) + it('T26:Fail to search resource booking with not allowed fields', async () => { + const data = testData.T26 + let error + try { + await service.searchResourceBookings(commonData.userWithManagePermission, data.criteria) + } catch (err) { + error = err + } + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + it('T27:Fail to search resource booking with memberRate', async () => { + const data = testData.T27 + let error + try { + await service.searchResourceBookings(commonData.regularUser, data.criteria) + } catch (err) { + error = err + } + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + it('T28:Fail to search resource booking with nested workPeriods', async () => { + const data = testData.T28 + let error + try { + await service.searchResourceBookings(commonData.currentUser, data.criteria) + } catch (err) { + error = err + } + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + it('T29:Fail to search resource booking without filtering by projectId as a regularUser', async () => { + const data = testData.T29 + let error + try { + await service.searchResourceBookings(commonData.regularUser, data.criteria) + } catch (err) { + error = err + } + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + it('T30:Fail to search resource booking as a regularUser who is not member of project', async () => { + const data = testData.T30 + const checkIsMemberOfProject = sinon.stub(helper, 'checkIsMemberOfProject').callsFake(() => { + throw new errors.UnauthorizedError(data.error.message) + }) + let error + try { + await service.searchResourceBookings(commonData.regularUser, data.criteria) + } catch (err) { + error = err + } + expect(checkIsMemberOfProject.calledOnce).to.be.true + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + it('T31:Fail to search resource booking with filtering by nested field', async () => { + const data = testData.T31 + let error + try { + await service.searchResourceBookings(commonData.userWithManagePermission, data.criteria) + } catch (err) { + error = err + } + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + it('T32:Fail to search resource booking with sorting by not included field', async () => { + const data = testData.T32 + let error + try { + await service.searchResourceBookings(commonData.userWithManagePermission, data.criteria) + } catch (err) { + error = err + } + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + it('T33:Fail to search resource booking with sorting by nested field', async () => { + const data = testData.T33 + let error + try { + await service.searchResourceBookings(commonData.userWithManagePermission, data.criteria) + } catch (err) { + error = err + } + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + it('T34:Fail to search resource booking with filtering by not included field', async () => { + const data = testData.T34 + let error + try { + await service.searchResourceBookings(commonData.userWithManagePermission, data.criteria) + } catch (err) { + error = err + } + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + it('T35:Fail to search resource booking with filtering by not included nested field', async () => { + const data = testData.T35 + let error + try { + await service.searchResourceBookings(commonData.userWithManagePermission, data.criteria) + } catch (err) { + error = err + } + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + }) + }) }) diff --git a/test/unit/common/CommonData.js b/test/unit/common/CommonData.js index 08f8aa66..2653e732 100644 --- a/test/unit/common/CommonData.js +++ b/test/unit/common/CommonData.js @@ -1,12 +1,23 @@ const currentUser = { userId: '00000000-0000-0000-0000-000000000000', - isMachine: true + isMachine: true, + scopes: [] } const UserTCConnCopilot = { userId: '4709473d-f060-4102-87f8-4d51ff0b34c1', handle: 'TCConnCopilot' } +const userWithManagePermission = { + hasManagePermission: true +} +const regularUser = { + userId: '222' +} +const ESClient = {} module.exports = { currentUser, - UserTCConnCopilot + UserTCConnCopilot, + userWithManagePermission, + regularUser, + ESClient } diff --git a/test/unit/common/ResourceBookingData.js b/test/unit/common/ResourceBookingData.js index 4679a835..b296a384 100644 --- a/test/unit/common/ResourceBookingData.js +++ b/test/unit/common/ResourceBookingData.js @@ -984,6 +984,371 @@ const T16 = { } } T16.resourceBooking.value.toJSON = () => T16.resourceBooking.value.dataValues +const T17 = { + esClientGet: { + body: { + _source: { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '05232809-3693-44c1-a0cc-9a79f2672385', + rateType: 'hourly', + createdAt: '2021-05-08T18:47:37.268Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: '60e99790-8da0-4596-badc-29a06feb78a0', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-05-08T18:47:37.268Z' + } + } + }, + criteria: {} +} +const T18 = { + resourceBooking: { + value: { + dataValues: { + id: '520bb632-a02a-415e-9857-93b2ecbf7d60', + projectId: 21, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '6093e58c-683d-4022-8482-5515e8345016', + startDate: '2021-04-05', + endDate: '2021-04-17', + memberRate: 13.23, + customerRate: 13, + rateType: 'hourly', + createdAt: '2020-10-09T04:24:01.048Z', + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + status: 'sourcing', + billingAccountId: 68800079 + } + } + }, + criteria: { fromDb: true } +} +const T19 = { + id: '520bb632-a02a-415e-9857-93b2ecbf7d60', + criteria: { + fields: 'other' + }, + error: { + httpStatus: 400, + message: 'other are not allowed' + } +} +const T20 = { + id: '520bb632-a02a-415e-9857-93b2ecbf7d60', + criteria: { + fields: 'memberRate' + }, + error: { + httpStatus: 403, + message: 'You don\'t have access to view memberRate' + } +} +const T21 = { + id: '520bb632-a02a-415e-9857-93b2ecbf7d60', + criteria: { + fields: 'workPeriods' + }, + error: { + httpStatus: 403, + message: 'You don\'t have access to view workPeriods' + } +} +const T22 = { + id: '520bb632-a02a-415e-9857-93b2ecbf7d60', + criteria: { + fields: 'id' + }, + error: { + httpStatus: 403, + message: 'Not allowed without including "projectId"' + } +} +const T23 = { + id: '520bb632-a02a-415e-9857-93b2ecbf7d60', + criteria: {}, + esClientGet: { + body: { + _source: { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '05232809-3693-44c1-a0cc-9a79f2672385', + rateType: 'hourly', + createdAt: '2021-05-08T18:47:37.268Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: '520bb632-a02a-415e-9857-93b2ecbf7d60', + projectId: 111, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-05-08T18:47:37.268Z' + } + } + }, + error: { + httpStatus: 401, + message: 'userId: 222 the user is not a member of project 111' + } +} +const T24 = { + esClientSearch: { + body: { + hits: { + total: { + value: 2 + }, + hits: [ + { + _source: { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '05232809-3693-44c1-a0cc-9a79f2672385', + rateType: 'hourly', + createdAt: '2021-05-08T18:35:16.368Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: 'fbe133dd-0e36-4d0c-8197-49307b13ce75', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-05-08T18:35:16.368Z' + } + }, + { + _source: { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '05232809-3693-44c1-a0cc-9a79f2672385', + rateType: 'hourly', + createdAt: '2021-05-08T18:47:37.268Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: '60e99790-8da0-4596-badc-29a06feb78a0', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-05-08T18:47:37.268Z' + } + } + ] + } + } + }, + criteria: {}, + result: { + total: 2, + page: 1, + perPage: 20, + result: [ + { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '05232809-3693-44c1-a0cc-9a79f2672385', + rateType: 'hourly', + createdAt: '2021-05-08T18:35:16.368Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: 'fbe133dd-0e36-4d0c-8197-49307b13ce75', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-05-08T18:35:16.368Z' + }, + { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '05232809-3693-44c1-a0cc-9a79f2672385', + rateType: 'hourly', + createdAt: '2021-05-08T18:47:37.268Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: '60e99790-8da0-4596-badc-29a06feb78a0', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-05-08T18:47:37.268Z' + } + ] + } +} +const T25 = { + resourceBookingFindAll: [ + { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '05232809-3693-44c1-a0cc-9a79f2672385', + rateType: 'hourly', + createdAt: '2021-05-08T18:35:16.368Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: 'fbe133dd-0e36-4d0c-8197-49307b13ce75', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-05-08T18:35:16.368Z' + }, + { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '05232809-3693-44c1-a0cc-9a79f2672385', + rateType: 'hourly', + createdAt: '2021-05-08T18:47:37.268Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: '60e99790-8da0-4596-badc-29a06feb78a0', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-05-08T18:47:37.268Z' + } + ], + criteria: {}, + result: { + fromDb: true, + total: 2, + page: 1, + perPage: 20, + result: [ + { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '05232809-3693-44c1-a0cc-9a79f2672385', + rateType: 'hourly', + createdAt: '2021-05-08T18:35:16.368Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: 'fbe133dd-0e36-4d0c-8197-49307b13ce75', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-05-08T18:35:16.368Z' + }, + { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '05232809-3693-44c1-a0cc-9a79f2672385', + rateType: 'hourly', + createdAt: '2021-05-08T18:47:37.268Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: '60e99790-8da0-4596-badc-29a06feb78a0', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-05-08T18:47:37.268Z' + } + ] + } +} +const T26 = { + criteria: { + fields: 'other' + }, + error: { + httpStatus: 400, + message: 'other are not allowed' + } +} +const T27 = { + criteria: { + fields: 'memberRate' + }, + error: { + httpStatus: 403, + message: 'You don\'t have access to view memberRate' + } +} +const T28 = { + criteria: { + fields: 'workPeriods' + }, + error: { + httpStatus: 403, + message: 'You don\'t have access to view workPeriods' + } +} +const T29 = { + criteria: {}, + error: { + httpStatus: 403, + message: 'Not allowed without filtering by "projectId"' + } +} +const T30 = { + criteria: { projectId: 111 }, + error: { + httpStatus: 401, + message: 'userId: 222 the user is not a member of project 111' + } +} +const T31 = { + criteria: { 'workPeriods.startDate': '2021-05-10' }, + error: { + httpStatus: 400, + message: 'Can not filter or sort by some field which is not included in fields' + } +} +const T32 = { + criteria: { sortBy: 'workPeriods.daysWorked' }, + error: { + httpStatus: 400, + message: 'Can not filter or sort by some field which is not included in fields' + } +} +const T33 = { + criteria: { fields: 'workPeriods', sortBy: 'workPeriods.daysWorked' }, + error: { + httpStatus: 400, + message: 'Can not sort by workPeriod field without filtering by workPeriods.startDate or workPeriods.endDate' + } +} +const T34 = { + criteria: { fields: 'id,startDate,endDate,workPeriods', status: 'closed' }, + error: { + httpStatus: 400, + message: 'Can not filter or sort by some field which is not included in fields' + } +} +const T35 = { + criteria: { fields: 'id,startDate,endDate', 'workPeriods.paymentStatus': 'completed' }, + error: { + httpStatus: 400, + message: 'Can not filter or sort by some field which is not included in fields' + } +} module.exports = { T01, T02, @@ -1000,5 +1365,24 @@ module.exports = { T13, T14, T15, - T16 + T16, + T17, + T18, + T19, + T20, + T21, + T22, + T23, + T24, + T25, + T26, + T27, + T28, + T29, + T30, + T31, + T32, + T33, + T34, + T35 } From c0fca43c2cc9a42ca0df666380d8b86379f60a82 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Mon, 17 May 2021 13:38:33 +0300 Subject: [PATCH 02/86] chore: use postgresql version as on PROD --- local/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/local/docker-compose.yml b/local/docker-compose.yml index 3894d100..34229bde 100644 --- a/local/docker-compose.yml +++ b/local/docker-compose.yml @@ -2,7 +2,7 @@ version: "3" services: postgres: container_name: tc-taas-postgres - image: postgres + image: postgres:11.8 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -85,4 +85,4 @@ services: - AUTH0_AUDIENCE - AUTH0_CLIENT_ID - AUTH0_CLIENT_SECRET - - AUTH0_PROXY_SERVER_URL + - AUTH0_PROXY_SERVER_URL \ No newline at end of file From 32101ccd826779857ce8956c639dca5c2fb5d786 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Tue, 18 May 2021 23:30:59 +0300 Subject: [PATCH 03/86] fix: not able to get RB without WB --- .../ResourceBookingEventHandler.js | 4 ++- src/services/ResourceBookingService.js | 28 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/eventHandlers/ResourceBookingEventHandler.js b/src/eventHandlers/ResourceBookingEventHandler.js index 3e8c18c3..ef5825fc 100644 --- a/src/eventHandlers/ResourceBookingEventHandler.js +++ b/src/eventHandlers/ResourceBookingEventHandler.js @@ -169,7 +169,9 @@ async function updateWorkPeriods (payload) { raw: true }) // gather workPeriod dates - const newWorkPeriods = helper.extractWorkPeriods(payload.value.startDate || payload.options.oldValue.startDate, payload.value.endDate || payload.options.oldValue.endDate) + const newWorkPeriods = helper.extractWorkPeriods( + _.isUndefined(payload.value.startDate) ? payload.options.oldValue.startDate : payload.value.startDate, + _.isUndefined(payload.value.endDate) ? payload.options.oldValue.endDate : payload.value.endDate) // find which workPeriods should be removed const workPeriodsToRemove = _.differenceBy(workPeriods, newWorkPeriods, 'startDate') // find which workperiods should be created diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 81c21fda..b9c7be2d 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -194,7 +194,9 @@ async function _ensurePaidWorkPeriodsNotDeleted (resourceBookingId, oldValue, ne return } // gather workPeriod dates from provided dates - const newWorkPeriods = helper.extractWorkPeriods(newValue.startDate || oldValue.startDate, newValue.endDate || oldValue.endDate) + const newWorkPeriods = helper.extractWorkPeriods( + _.isUndefined(newValue.startDate) ? oldValue.startDate : newValue.startDate, + _.isUndefined(newValue.endDate) ? oldValue.endDate : newValue.endDate) // find which workPeriods should be removed const workPeriodsToRemove = _.differenceBy(workPeriods, newWorkPeriods, 'startDate') // we can't delete workperiods with paymentStatus 'partially-completed' or 'completed'. @@ -477,14 +479,7 @@ async function searchResourceBookings (currentUser, criteria, options = { return body: { query: { bool: { - must: [ - { - nested: { - path: 'workPeriods', - query: { bool: { must: [] } } - } - } - ] + must: [] } }, from: (page - 1) * perPage, @@ -525,9 +520,18 @@ async function searchResourceBookings (currentUser, criteria, options = { return } }] } + const workPeriodFilters = ['workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle'] + if (_.includes(criteria, workPeriodFilters)) { + esQuery.body.query.bool.must.push({ + nested: { + path: 'workPeriods', + query: { bool: { must: [] } } + } + }) + } // Apply WorkPeriod filters - _.each(_.pick(criteria, ['workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle']), (value, key) => { - esQuery.body.query.bool.must[0].nested.query.bool.must.push({ + _.each(_.pick(criteria, workPeriodFilters), (value, key) => { + esQuery.body.query.bool.must[esQuery.body.query.bool.must.length - 1].nested.query.bool.must.push({ term: { [key]: { value @@ -541,7 +545,7 @@ async function searchResourceBookings (currentUser, criteria, options = { return let resourceBookings = _.map(body.hits.hits, '_source') // ESClient will return ResourceBookings with it's all nested WorkPeriods // We re-apply WorkPeriod filters - _.each(_.pick(criteria, ['workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle', 'workPeriods.paymentStatus']), (value, key) => { + _.each(_.pick(criteria, workPeriodFilters), (value, key) => { key = key.split('.')[1] _.each(resourceBookings, r => { r.workPeriods = _.filter(r.workPeriods, { [key]: value }) From 4de085d3192c98fc669bea71bf34b6f885b0a11f Mon Sep 17 00:00:00 2001 From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com> Date: Wed, 19 May 2021 15:27:34 +0530 Subject: [PATCH 04/86] BA is to string [skip ci] BA is to string [skip ci] --- src/services/PaymentService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/PaymentService.js b/src/services/PaymentService.js index 3143b936..2dcd8b04 100644 --- a/src/services/PaymentService.js +++ b/src/services/PaymentService.js @@ -84,7 +84,7 @@ async function createChallenge (challenge, token) { if (challenge.billingAccountId) { body.billing = { - billingAccountId: challenge.billingAccountId, + billingAccountId: _.toString(challenge.billingAccountId), markup: 0 // for TaaS payments we always use 0 markup } } From 6a894b71aa1d472f29be2691e25f181a1506df54 Mon Sep 17 00:00:00 2001 From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com> Date: Wed, 19 May 2021 15:36:55 +0530 Subject: [PATCH 05/86] BA number to string --- src/services/PaymentService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/PaymentService.js b/src/services/PaymentService.js index 2dcd8b04..d06ad671 100644 --- a/src/services/PaymentService.js +++ b/src/services/PaymentService.js @@ -85,7 +85,7 @@ async function createChallenge (challenge, token) { if (challenge.billingAccountId) { body.billing = { billingAccountId: _.toString(challenge.billingAccountId), - markup: 0 // for TaaS payments we always use 0 markup + markup: 0 // for TaaS payments we always use 0 markup } } try { From 7783aa321fea82ec9c9b1b320b81f4fcb0b34747 Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Fri, 21 May 2021 17:35:18 +0530 Subject: [PATCH 06/86] Adding statuses --- src/bootstrap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bootstrap.js b/src/bootstrap.js index 6a364e8a..2999f131 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -16,7 +16,7 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') -Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected') +Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed') Joi.title = () => Joi.string().max(128) Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) From dc1cfa43216d85b328b0697927e6087d11efa988 Mon Sep 17 00:00:00 2001 From: dengjun Date: Sat, 22 May 2021 16:16:26 +0800 Subject: [PATCH 07/86] Add JobIds to /jobs endpoint:30185451 --- ...coder-bookings-api.postman_collection.json | 109 +++++++++++++++++- docs/swagger.yaml | 65 +++++++++-- src/controllers/JobController.js | 3 + src/services/JobService.js | 23 +++- 4 files changed, 186 insertions(+), 14 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index a6afdc2b..c6d395bb 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "3f80d93d-6ca3-4645-970d-a9e533394e2e", + "_postman_id": "4b866040-1336-427b-9e20-56d809b87519", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -491,6 +491,113 @@ }, "response": [] }, + { + "name": "search jobs with request body", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"jobIds\": [\"{{jobId}}\",\"{{jobIdCreatedByM2M}}\"]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobs", + "host": [ + "{{URL}}" + ], + "path": [ + "jobs" + ], + "query": [ + { + "key": "page", + "value": "0", + "disabled": true + }, + { + "key": "perPage", + "value": "3", + "disabled": true + }, + { + "key": "sortBy", + "value": "id", + "disabled": true + }, + { + "key": "sortOrder", + "value": "asc", + "disabled": true + }, + { + "key": "projectId", + "value": "21", + "disabled": true + }, + { + "key": "externalId", + "value": "1212", + "disabled": true + }, + { + "key": "description", + "value": "Dummy", + "disabled": true + }, + { + "key": "startDate", + "value": "2020-09-27T04:17:23.131Z", + "disabled": true + }, + { + "key": "resourceType", + "value": "Dummy Resource Type", + "disabled": true + }, + { + "key": "skill", + "value": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "disabled": true + }, + { + "key": "rateType", + "value": "hourly", + "disabled": true + }, + { + "key": "status", + "value": "sourcing", + "disabled": true + }, + { + "key": "workload", + "value": "full-time", + "disabled": true + }, + { + "key": "title", + "value": "dummy", + "disabled": true + } + ] + } + }, + "response": [] + }, { "name": "search jobs with with m2m all", "request": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 0a9fe80d..3a6ce840 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -181,6 +181,11 @@ paths: type: string enum: ["sourcing", "in-review", "assigned", "closed", "cancelled"] description: The rate type. + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/JobSearchBody" responses: "200": description: OK @@ -3317,6 +3322,15 @@ components: type: string example: "topcoder user" description: "The user who updated the job last time.(Will get the user info from the token)" + JobSearchBody: + properties: + jobIds: + type: array + items: + type: string + format: uuid + description: "The array of job ids" + JobRequestBody: required: - projectId @@ -3517,7 +3531,18 @@ components: description: "The user id." status: type: string - enum: ["open", "placed", "selected", "client rejected - screening", "client rejected - interview", "rejected - other", "cancelled", "interview", "topcoder-rejected"] + enum: + [ + "open", + "placed", + "selected", + "client rejected - screening", + "client rejected - interview", + "rejected - other", + "cancelled", + "interview", + "topcoder-rejected", + ] description: "The job candidate status." default: open externalId: @@ -3632,7 +3657,15 @@ components: description: "Interview end time." status: type: string - enum: ["Scheduling", "Scheduled", "Requested for reschedule", "Rescheduled", "Completed", "Cancelled"] + enum: + [ + "Scheduling", + "Scheduled", + "Requested for reschedule", + "Rescheduled", + "Completed", + "Cancelled", + ] description: "The interview status." rescheduleUrl: type: string @@ -3678,7 +3711,15 @@ components: format: email status: type: string - enum: ["Scheduling", "Scheduled", "Requested for reschedule", "Rescheduled", "Completed", "Cancelled"] + enum: + [ + "Scheduling", + "Scheduled", + "Requested for reschedule", + "Rescheduled", + "Completed", + "Cancelled", + ] default: "Scheduling" description: "The interview status." UpdateInterviewRequestBody: @@ -3737,7 +3778,15 @@ components: description: "Interview end time." status: type: string - enum: ["Scheduling", "Scheduled", "Requested for reschedule", "Rescheduled", "Completed", "Cancelled"] + enum: + [ + "Scheduling", + "Scheduled", + "Requested for reschedule", + "Rescheduled", + "Completed", + "Cancelled", + ] description: "The interview status." rescheduleUrl: type: string @@ -3845,7 +3894,7 @@ components: billingAccountId: type: integer example: 80000071 - description: 'the billing account id for payments' + description: "the billing account id for payments" createdAt: type: string format: date-time @@ -3913,7 +3962,7 @@ components: billingAccountId: type: integer example: 80000071 - description: 'the billing account id for payments' + description: "the billing account id for payments" ResourceBookingPatchRequestBody: properties: status: @@ -3946,7 +3995,7 @@ components: billingAccountId: type: integer example: 80000071 - description: 'the billing account id for payments' + description: "the billing account id for payments" WorkPeriod: required: - id @@ -4130,7 +4179,7 @@ components: billingAccountId: type: integer example: 80000071 - description: 'the billing account id for payments' + description: "the billing account id for payments" createdAt: type: string format: date-time diff --git a/src/controllers/JobController.js b/src/controllers/JobController.js index 14f5cfc5..b7cad958 100644 --- a/src/controllers/JobController.js +++ b/src/controllers/JobController.js @@ -57,6 +57,9 @@ async function deleteJob (req, res) { * @param res the response */ async function searchJobs (req, res) { + if (req.body && req.body.jobIds) { + req.query.jobIds = req.body.jobIds + } const result = await service.searchJobs(req.authUser, req.query) helper.setResHeaders(req, res, result) res.send(result.result) diff --git a/src/services/JobService.js b/src/services/JobService.js index 7d855bd0..61685901 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -344,7 +344,8 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } body: { query: { bool: { - must: [] + must: [], + filter: [] } }, from: (page - 1) * perPage, @@ -393,11 +394,19 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } }) // If criteria contains projectIds, filter projectId with this value if (criteria.projectIds) { - esQuery.body.query.bool.filter = [{ + esQuery.body.query.bool.filter.push({ terms: { projectId: criteria.projectIds } - }] + }) + } + // if criteria contains jobIds, filter jobIds with this value + if (criteria.jobIds && criteria.jobIds.length > 0) { + esQuery.body.query.bool.filter.push({ + terms: { + _id: criteria.jobIds + } + }) } logger.debug({ component: 'JobService', context: 'searchJobs', message: `Query: ${JSON.stringify(esQuery)}` }) @@ -422,7 +431,7 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } logger.logFullError(err, { component: 'JobService', context: 'searchJobs' }) } logger.info({ component: 'JobService', context: 'searchJobs', message: 'fallback to DB query' }) - const filter = {} + const filter = { [Op.and]: [] } _.each(_.pick(criteria, [ 'projectId', 'externalId', @@ -449,6 +458,9 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } [Op.contains]: [criteria.skills] } } + if (criteria.jobIds && criteria.jobIds.length > 0) { + filter[Op.and].push({ id: criteria.jobIds }) + } const jobs = await Job.findAll({ where: filter, offset: ((page - 1) * perPage), @@ -486,7 +498,8 @@ searchJobs.schema = Joi.object().keys({ rateType: Joi.rateType(), workload: Joi.workload(), status: Joi.jobStatus(), - projectIds: Joi.array().items(Joi.number().integer()).single() + projectIds: Joi.array().items(Joi.number().integer()).single(), + jobIds: Joi.array().items(Joi.string().uuid()) }).required(), options: Joi.object() }).required() From 8b55765ea32bf94debec6a9e27cc4e6f223058dd Mon Sep 17 00:00:00 2001 From: eisbilir Date: Sat, 22 May 2021 21:03:45 +0300 Subject: [PATCH 08/86] fix: checking if member of project --- src/services/WorkPeriodService.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index 19346ff3..fc750bd7 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -177,8 +177,9 @@ async function getWorkPeriod (currentUser, id, fromDb = false) { if (!resourceBooking.body.hits.total.value) { throw new errors.NotFoundError() } - await _checkUserPermissionForGetWorkPeriod(currentUser, resourceBooking.body.hits.hits[0]._source.workPeriods.projectId) // check user permission - return _.find(resourceBooking.body.hits.hits[0]._source.workPeriods, { id }) + const workPeriod = _.find(resourceBooking.body.hits.hits[0]._source.workPeriods, { id }) + await _checkUserPermissionForGetWorkPeriod(currentUser, workPeriod.projectId) // check user permission + return workPeriod } catch (err) { if (helper.isDocumentMissingException(err)) { throw new errors.NotFoundError(`id: ${id} "WorkPeriod" not found`) From ba0af40d929132fb438ca68ea59013b7e24472a3 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sat, 22 May 2021 23:21:28 +0300 Subject: [PATCH 09/86] fix: return pagination headers when fallback to DB --- src/common/helper.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 234e96fe..f7bcb148 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -599,9 +599,6 @@ function getPageLink (req, page) { * @param {Object} result the operation result */ function setResHeaders (req, res, result) { - if (result.fromDb) { - return - } const totalPages = Math.ceil(result.total / result.perPage) if (result.page > 1) { res.set('X-Prev-Page', result.page - 1) From 479b790737c24d63c4e5c2759676ae25d041b067 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sat, 22 May 2021 23:24:24 +0300 Subject: [PATCH 10/86] fix: filter WorkPeriods in RB endpoint before this fix it returned error like 'cannot get "nested" of undefined' --- src/services/ResourceBookingService.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index b9c7be2d..d8fd2e5c 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -520,25 +520,27 @@ async function searchResourceBookings (currentUser, criteria, options = { return } }] } + // Apply WorkPeriod filters const workPeriodFilters = ['workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle'] - if (_.includes(criteria, workPeriodFilters)) { + if (_.intersection(criteria, workPeriodFilters).length > 0) { + const workPeriodsMust = [] + _.each(_.pick(criteria, workPeriodFilters), (value, key) => { + workPeriodsMust.push({ + term: { + [key]: { + value + } + } + }) + }) + esQuery.body.query.bool.must.push({ nested: { path: 'workPeriods', - query: { bool: { must: [] } } + query: { bool: { must: workPeriodsMust } } } }) } - // Apply WorkPeriod filters - _.each(_.pick(criteria, workPeriodFilters), (value, key) => { - esQuery.body.query.bool.must[esQuery.body.query.bool.must.length - 1].nested.query.bool.must.push({ - term: { - [key]: { - value - } - } - }) - }) logger.debug({ component: 'ResourceBookingService', context: 'searchResourceBookings', message: `Query: ${JSON.stringify(esQuery)}` }) const { body } = await esClient.search(esQuery) From cb73c9931a1a06011a3e0c0fa73d554226e21de0 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sun, 23 May 2021 11:32:17 +0300 Subject: [PATCH 11/86] chore: force fallback to DB in RB requests --- src/services/ResourceBookingService.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index d8fd2e5c..5d1bd085 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -472,6 +472,7 @@ async function searchResourceBookings (currentUser, criteria, options = { return criteria.sortOrder = 'desc' } try { + throw new Error('fallback to DB') const esQuery = { index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), _source_includes: queryOpt.include, From 5790aba6b2866286dd900a4973f5c3999b19e9f6 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sun, 23 May 2021 11:39:24 +0300 Subject: [PATCH 12/86] fix: perPage from DB --- src/services/ResourceBookingService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 5d1bd085..bab63925 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -618,7 +618,7 @@ async function searchResourceBookings (currentUser, criteria, options = { return } else { queryCriteria.order = [[{ model: WorkPeriod, as: 'workPeriods' }, _.split(criteria.sortBy, '.')[1], criteria.sortOrder]] } - const resourceBookings = await ResourceBooking.findAll(queryCriteria) + const resourceBookings = await ResourceBooking.findAll(_.omit(queryCriteria, ['limit', 'offset'])) return { fromDb: true, total: resourceBookings.length, From 39527ad4cd98ad816e23f47773fee5a9f635bf93 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sun, 23 May 2021 13:24:22 +0300 Subject: [PATCH 13/86] fix: "total" value and DB pagination --- src/services/ResourceBookingService.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index bab63925..f5c40206 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -618,10 +618,11 @@ async function searchResourceBookings (currentUser, criteria, options = { return } else { queryCriteria.order = [[{ model: WorkPeriod, as: 'workPeriods' }, _.split(criteria.sortBy, '.')[1], criteria.sortOrder]] } - const resourceBookings = await ResourceBooking.findAll(_.omit(queryCriteria, ['limit', 'offset'])) + const resourceBookings = await ResourceBooking.findAll(queryCriteria) + const total = await ResourceBooking.count(_.omit(queryCriteria, ['limit', 'offset', 'attributes', 'order'])) return { fromDb: true, - total: resourceBookings.length, + total, page, perPage, result: resourceBookings From 3a2a3d39b9600bb4a134766143514b6aa8714af2 Mon Sep 17 00:00:00 2001 From: Michael Baghel Date: Mon, 24 May 2021 23:11:45 +0400 Subject: [PATCH 14/86] Created new route for creating a team/project --- src/common/helper.js | 1327 ++++++++++++++++++----------- src/controllers/TeamController.js | 84 +- src/routes/TeamRoutes.js | 52 +- src/services/TeamService.js | 693 +++++++++------ 4 files changed, 1317 insertions(+), 839 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 234e96fe..b8dc9c5a 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -2,63 +2,103 @@ * This file defines helper methods */ -const fs = require('fs') -const querystring = require('querystring') -const Confirm = require('prompt-confirm') -const Bottleneck = require('bottleneck') -const AWS = require('aws-sdk') -const config = require('config') -const HttpStatus = require('http-status-codes') -const _ = require('lodash') -const request = require('superagent') -const elasticsearch = require('@elastic/elasticsearch') -const { ResponseError: ESResponseError } = require('@elastic/elasticsearch/lib/errors') -const errors = require('../common/errors') -const logger = require('./logger') -const models = require('../models') -const eventDispatcher = require('./eventDispatcher') -const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') -const moment = require('moment') +const fs = require('fs'); +const querystring = require('querystring'); +const Confirm = require('prompt-confirm'); +const Bottleneck = require('bottleneck'); +const AWS = require('aws-sdk'); +const config = require('config'); +const HttpStatus = require('http-status-codes'); +const _ = require('lodash'); +const request = require('superagent'); +const elasticsearch = require('@elastic/elasticsearch'); +const { + ResponseError: ESResponseError, +} = require('@elastic/elasticsearch/lib/errors'); +const errors = require('../common/errors'); +const logger = require('./logger'); +const models = require('../models'); +const eventDispatcher = require('./eventDispatcher'); +const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper'); +const moment = require('moment'); const localLogger = { - debug: (message) => logger.debug({ component: 'helper', context: message.context, message: message.message }), - error: (message) => logger.error({ component: 'helper', context: message.context, message: message.message }), - info: (message) => logger.info({ component: 'helper', context: message.context, message: message.message }) -} - -AWS.config.region = config.esConfig.AWS_REGION - -const m2mAuth = require('tc-core-library-js').auth.m2m - -const m2m = m2mAuth(_.pick(config, ['AUTH0_URL', 'AUTH0_AUDIENCE', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', 'AUTH0_PROXY_SERVER_URL'])) + debug: (message) => + logger.debug({ + component: 'helper', + context: message.context, + message: message.message, + }), + error: (message) => + logger.error({ + component: 'helper', + context: message.context, + message: message.message, + }), + info: (message) => + logger.info({ + component: 'helper', + context: message.context, + message: message.message, + }), +}; + +AWS.config.region = config.esConfig.AWS_REGION; + +const m2mAuth = require('tc-core-library-js').auth.m2m; + +const m2m = m2mAuth( + _.pick(config, [ + 'AUTH0_URL', + 'AUTH0_AUDIENCE', + 'AUTH0_CLIENT_ID', + 'AUTH0_CLIENT_SECRET', + 'AUTH0_PROXY_SERVER_URL', + ]) +); const m2mForUbahn = m2mAuth({ AUTH0_AUDIENCE: config.AUTH0_AUDIENCE_UBAHN, - ..._.pick(config, ['AUTH0_URL', 'TOKEN_CACHE_TIME', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', 'AUTH0_PROXY_SERVER_URL']) -} -) + ..._.pick(config, [ + 'AUTH0_URL', + 'TOKEN_CACHE_TIME', + 'AUTH0_CLIENT_ID', + 'AUTH0_CLIENT_SECRET', + 'AUTH0_PROXY_SERVER_URL', + ]), +}); -let busApiClient +let busApiClient; /** * Get bus api client. * * @returns {Object} the bus api client */ -function getBusApiClient () { +function getBusApiClient() { if (busApiClient) { - return busApiClient + return busApiClient; } - busApiClient = busApi(_.pick(config, ['AUTH0_URL', 'AUTH0_AUDIENCE', 'TOKEN_CACHE_TIME', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', 'BUSAPI_URL', 'KAFKA_ERROR_TOPIC', 'AUTH0_PROXY_SERVER_URL']) - ) - return busApiClient + busApiClient = busApi( + _.pick(config, [ + 'AUTH0_URL', + 'AUTH0_AUDIENCE', + 'TOKEN_CACHE_TIME', + 'AUTH0_CLIENT_ID', + 'AUTH0_CLIENT_SECRET', + 'BUSAPI_URL', + 'KAFKA_ERROR_TOPIC', + 'AUTH0_PROXY_SERVER_URL', + ]) + ); + return busApiClient; } // ES Client mapping -const esClients = {} +const esClients = {}; // The es index property mapping -const esIndexPropertyMapping = {} +const esIndexPropertyMapping = {}; esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { projectId: { type: 'integer' }, externalId: { type: 'keyword' }, @@ -76,8 +116,8 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } -} + updatedBy: { type: 'keyword' }, +}; esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { jobId: { type: 'keyword' }, userId: { type: 'keyword' }, @@ -110,14 +150,14 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, updatedBy: { type: 'keyword' }, - deletedAt: { type: 'date' } - } + deletedAt: { type: 'date' }, + }, }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } -} + updatedBy: { type: 'keyword' }, +}; esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { projectId: { type: 'integer' }, userId: { type: 'keyword' }, @@ -155,32 +195,32 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } - } + updatedBy: { type: 'keyword' }, + }, }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } - } + updatedBy: { type: 'keyword' }, + }, }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } -} + updatedBy: { type: 'keyword' }, +}; /** * Get the first parameter from cli arguments */ -function getParamFromCliArgs () { - const filteredArgs = process.argv.filter(arg => !arg.includes('--')) +function getParamFromCliArgs() { + const filteredArgs = process.argv.filter((arg) => !arg.includes('--')); if (filteredArgs.length > 2) { - return filteredArgs[2] + return filteredArgs[2]; } - return null + return null; } /** @@ -188,18 +228,18 @@ function getParamFromCliArgs () { * @param {string} promptQuery the query to ask the user * @param {function} cb the callback function */ -async function promptUser (promptQuery, cb) { +async function promptUser(promptQuery, cb) { if (process.argv.includes('--force')) { - await cb() - return + await cb(); + return; } - const prompt = new Confirm(promptQuery) + const prompt = new Confirm(promptQuery); prompt.ask(async (answer) => { if (answer) { - await cb() + await cb(); } - }) + }); } /** @@ -208,20 +248,23 @@ async function promptUser (promptQuery, cb) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function createIndex (index, logger, esClient = null) { +async function createIndex(index, logger, esClient = null) { if (!esClient) { - esClient = getESClient() + esClient = getESClient(); } await esClient.indices.create({ index, body: { mappings: { - properties: esIndexPropertyMapping[index] - } - } - }) - logger.info({ component: 'createIndex', message: `ES Index ${index} creation succeeded!` }) + properties: esIndexPropertyMapping[index], + }, + }, + }); + logger.info({ + component: 'createIndex', + message: `ES Index ${index} creation succeeded!`, + }); } /** @@ -230,105 +273,133 @@ async function createIndex (index, logger, esClient = null) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function deleteIndex (index, logger, esClient = null) { +async function deleteIndex(index, logger, esClient = null) { if (!esClient) { - esClient = getESClient() + esClient = getESClient(); } - await esClient.indices.delete({ index }) - logger.info({ component: 'deleteIndex', message: `ES Index ${index} deletion succeeded!` }) + await esClient.indices.delete({ index }); + logger.info({ + component: 'deleteIndex', + message: `ES Index ${index} deletion succeeded!`, + }); } /** * Split data into bulks * @param {Array} data the array of data to split */ -function getBulksFromDocuments (data) { - const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6 - const bulks = [] - let documentIndex = 0 - let currentBulkSize = 0 - let currentBulk = [] +function getBulksFromDocuments(data) { + const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6; + const bulks = []; + let documentIndex = 0; + let currentBulkSize = 0; + let currentBulk = []; while (true) { // break loop when parsed all documents if (documentIndex >= data.length) { - bulks.push(currentBulk) - break + bulks.push(currentBulk); + break; } // check if current document size is greater than the max bulk size, if so, throw error - const currentDocumentSize = Buffer.byteLength(JSON.stringify(data[documentIndex]), 'utf-8') + const currentDocumentSize = Buffer.byteLength( + JSON.stringify(data[documentIndex]), + 'utf-8' + ); if (maxBytes < currentDocumentSize) { - throw new Error(`Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.`) + throw new Error( + `Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.` + ); } - if (currentBulkSize + currentDocumentSize > maxBytes || - currentBulk.length >= config.get('esConfig.MAX_BULK_NUM_DOCUMENTS')) { + if ( + currentBulkSize + currentDocumentSize > maxBytes || + currentBulk.length >= config.get('esConfig.MAX_BULK_NUM_DOCUMENTS') + ) { // if adding the current document goes over the max bulk size OR goes over max number of docs // then push the current bulk to bulks array and reset the current bulk - bulks.push(currentBulk) - currentBulk = [] - currentBulkSize = 0 + bulks.push(currentBulk); + currentBulk = []; + currentBulkSize = 0; } else { // otherwise, add document to current bulk - currentBulk.push(data[documentIndex]) - currentBulkSize += currentDocumentSize - documentIndex++ + currentBulk.push(data[documentIndex]); + currentBulkSize += currentDocumentSize; + documentIndex++; } } - return bulks + return bulks; } /** -* Index records in bulk -* @param {Object | String} modelOpts the model name in db, or model options -* @param {Object} indexName the index name -* @param {Object} logger the logger object -*/ -async function indexBulkDataToES (modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName - const include = _.get(modelOpts, 'include', []) + * Index records in bulk + * @param {Object | String} modelOpts the model name in db, or model options + * @param {Object} indexName the index name + * @param {Object} logger the logger object + */ +async function indexBulkDataToES(modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; + const include = _.get(modelOpts, 'include', []); - logger.info({ component: 'indexBulkDataToES', message: `Reindexing of ${modelName}s started!` }) + logger.info({ + component: 'indexBulkDataToES', + message: `Reindexing of ${modelName}s started!`, + }); - const esClient = getESClient() + const esClient = getESClient(); // clear index - const indexExistsRes = await esClient.indices.exists({ index: indexName }) + const indexExistsRes = await esClient.indices.exists({ index: indexName }); if (indexExistsRes.statusCode !== 404) { - await deleteIndex(indexName, logger, esClient) + await deleteIndex(indexName, logger, esClient); } - await createIndex(indexName, logger, esClient) + await createIndex(indexName, logger, esClient); // get data from db - logger.info({ component: 'indexBulkDataToES', message: 'Getting data from database' }) - const model = models[modelName] - const data = await model.findAll({ include }) - const rawObjects = _.map(data, r => r.toJSON()) + logger.info({ + component: 'indexBulkDataToES', + message: 'Getting data from database', + }); + const model = models[modelName]; + const data = await model.findAll({ include }); + const rawObjects = _.map(data, (r) => r.toJSON()); if (_.isEmpty(rawObjects)) { - logger.info({ component: 'indexBulkDataToES', message: `No data in database for ${modelName}` }) - return + logger.info({ + component: 'indexBulkDataToES', + message: `No data in database for ${modelName}`, + }); + return; } - const bulks = getBulksFromDocuments(rawObjects) + const bulks = getBulksFromDocuments(rawObjects); - const startTime = Date.now() - let doneCount = 0 + const startTime = Date.now(); + let doneCount = 0; for (const bulk of bulks) { // send bulk to esclient - const body = bulk.flatMap(doc => [{ index: { _index: indexName, _id: doc.id } }, doc]) - await esClient.bulk({ refresh: true, body }) - doneCount += bulk.length + const body = bulk.flatMap((doc) => [ + { index: { _index: indexName, _id: doc.id } }, + doc, + ]); + await esClient.bulk({ refresh: true, body }); + doneCount += bulk.length; // log metrics - const timeSpent = Date.now() - startTime - const avgTimePerDocument = timeSpent / doneCount - const estimatedLength = (avgTimePerDocument * data.length) - const timeLeft = (startTime + estimatedLength) - Date.now() + const timeSpent = Date.now() - startTime; + const avgTimePerDocument = timeSpent / doneCount; + const estimatedLength = avgTimePerDocument * data.length; + const timeLeft = startTime + estimatedLength - Date.now(); logger.info({ component: 'indexBulkDataToES', - message: `Processed ${doneCount} of ${data.length} documents, average time per document ${formatTime(avgTimePerDocument)}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime(timeLeft)}` - }) + message: `Processed ${doneCount} of ${ + data.length + } documents, average time per document ${formatTime( + avgTimePerDocument + )}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime( + timeLeft + )}`, + }); } } @@ -339,24 +410,36 @@ async function indexBulkDataToES (modelOpts, indexName, logger) { * @param {string} id the job id * @param {Object} logger the logger object */ -async function indexDataToEsById (id, modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName - const include = _.get(modelOpts, 'include', []) - - logger.info({ component: 'indexDataToEsById', message: `Reindexing of ${modelName} with id ${id} started!` }) - const esClient = getESClient() - - logger.info({ component: 'indexDataToEsById', message: 'Getting data from database' }) - const model = models[modelName] - - const data = await model.findById(id, include) - logger.info({ component: 'indexDataToEsById', message: 'Indexing data into Elasticsearch' }) +async function indexDataToEsById(id, modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; + const include = _.get(modelOpts, 'include', []); + + logger.info({ + component: 'indexDataToEsById', + message: `Reindexing of ${modelName} with id ${id} started!`, + }); + const esClient = getESClient(); + + logger.info({ + component: 'indexDataToEsById', + message: 'Getting data from database', + }); + const model = models[modelName]; + + const data = await model.findById(id, include); + logger.info({ + component: 'indexDataToEsById', + message: 'Indexing data into Elasticsearch', + }); await esClient.index({ index: indexName, id: id, - body: data.dataValues - }) - logger.info({ component: 'indexDataToEsById', message: 'Indexing complete!' }) + body: data.dataValues, + }); + logger.info({ + component: 'indexDataToEsById', + message: 'Indexing complete!', + }); } /** @@ -365,50 +448,68 @@ async function indexDataToEsById (id, modelOpts, indexName, logger) { * @param {Array} dataModels the data models to import * @param {Object} logger the logger object */ -async function importData (pathToFile, dataModels, logger) { +async function importData(pathToFile, dataModels, logger) { // check if file exists if (!fs.existsSync(pathToFile)) { - throw new Error(`File with path ${pathToFile} does not exist`) + throw new Error(`File with path ${pathToFile} does not exist`); } // clear database - logger.info({ component: 'importData', message: 'Clearing database...' }) - await models.sequelize.sync({ force: true }) + logger.info({ component: 'importData', message: 'Clearing database...' }); + await models.sequelize.sync({ force: true }); - let transaction = null - let currentModelName = null + let transaction = null; + let currentModelName = null; try { // Start a transaction - transaction = await models.sequelize.transaction() - const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()) + transaction = await models.sequelize.transaction(); + const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()); for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index] - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName - const include = _.get(modelOpts, 'include', []) + const modelOpts = dataModels[index]; + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; + const include = _.get(modelOpts, 'include', []); - currentModelName = modelName - const model = models[modelName] - const modelRecords = jsonData[modelName] + currentModelName = modelName; + const model = models[modelName]; + const modelRecords = jsonData[modelName]; if (modelRecords && modelRecords.length > 0) { - logger.info({ component: 'importData', message: `Importing data for model: ${modelName}` }) - - await model.bulkCreate(modelRecords, { include, transaction }) - logger.info({ component: 'importData', message: `Records imported for model: ${modelName} = ${modelRecords.length}` }) + logger.info({ + component: 'importData', + message: `Importing data for model: ${modelName}`, + }); + + await model.bulkCreate(modelRecords, { include, transaction }); + logger.info({ + component: 'importData', + message: `Records imported for model: ${modelName} = ${modelRecords.length}`, + }); } else { - logger.info({ component: 'importData', message: `No records to import for model: ${modelName}` }) + logger.info({ + component: 'importData', + message: `No records to import for model: ${modelName}`, + }); } } // commit transaction only if all things went ok - logger.info({ component: 'importData', message: 'committing transaction to database...' }) - await transaction.commit() + logger.info({ + component: 'importData', + message: 'committing transaction to database...', + }); + await transaction.commit(); } catch (error) { - logger.error({ component: 'importData', message: `Error while writing data of model: ${currentModelName}` }) + logger.error({ + component: 'importData', + message: `Error while writing data of model: ${currentModelName}`, + }); // rollback all insert operations if (transaction) { - logger.info({ component: 'importData', message: 'rollback database transaction...' }) - transaction.rollback() + logger.info({ + component: 'importData', + message: 'rollback database transaction...', + }); + transaction.rollback(); } if (error.name && error.errors && error.fields) { // For sequelize validation errors, we throw only fields with data that helps in debugging error, @@ -418,36 +519,50 @@ async function importData (pathToFile, dataModels, logger) { modelName: currentModelName, name: error.name, errors: error.errors, - fields: error.fields + fields: error.fields, }) - ) + ); } else { - throw error + throw error; } } // after importing, index data const jobCandidateModelOpts = { modelName: 'JobCandidate', - include: [{ - model: models.Interview, - as: 'interviews' - }] - } + include: [ + { + model: models.Interview, + as: 'interviews', + }, + ], + }; const resourceBookingModelOpts = { modelName: 'ResourceBooking', - include: [{ - model: models.WorkPeriod, - as: 'workPeriods', - include: [{ - model: models.WorkPeriodPayment, - as: 'payments' - }] - }] - } - await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) - await indexBulkDataToES(jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger) - await indexBulkDataToES(resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger) + include: [ + { + model: models.WorkPeriod, + as: 'workPeriods', + include: [ + { + model: models.WorkPeriodPayment, + as: 'payments', + }, + ], + }, + ], + }; + await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger); + await indexBulkDataToES( + jobCandidateModelOpts, + config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), + logger + ); + await indexBulkDataToES( + resourceBookingModelOpts, + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + logger + ); } /** @@ -456,65 +571,74 @@ async function importData (pathToFile, dataModels, logger) { * @param {Array} dataModels the data models to export * @param {Object} logger the logger object */ -async function exportData (pathToFile, dataModels, logger) { - logger.info({ component: 'exportData', message: `Start Saving data to file with path ${pathToFile}....` }) +async function exportData(pathToFile, dataModels, logger) { + logger.info({ + component: 'exportData', + message: `Start Saving data to file with path ${pathToFile}....`, + }); - const allModelsRecords = {} + const allModelsRecords = {}; for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index] - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName - const include = _.get(modelOpts, 'include', []) - const modelRecords = await models[modelName].findAll({ include }) - const rawRecords = _.map(modelRecords, r => r.toJSON()) - allModelsRecords[modelName] = rawRecords - logger.info({ component: 'exportData', message: `Records loaded for model: ${modelName} = ${rawRecords.length}` }) + const modelOpts = dataModels[index]; + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; + const include = _.get(modelOpts, 'include', []); + const modelRecords = await models[modelName].findAll({ include }); + const rawRecords = _.map(modelRecords, (r) => r.toJSON()); + allModelsRecords[modelName] = rawRecords; + logger.info({ + component: 'exportData', + message: `Records loaded for model: ${modelName} = ${rawRecords.length}`, + }); } - fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)) - logger.info({ component: 'exportData', message: 'End Saving data to file....' }) + fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)); + logger.info({ + component: 'exportData', + message: 'End Saving data to file....', + }); } /** * Format a time in milliseconds into a human readable format * @param {Date} milliseconds the number of milliseconds */ -function formatTime (millisec) { - const ms = Math.floor(millisec % 1000) - const secs = Math.floor((millisec / 1000) % 60) - const mins = Math.floor((millisec / (1000 * 60)) % 60) - const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24) - const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7) - const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4) - const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12) - const yrs = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12))) - - let formattedTime = '0 milliseconds' +function formatTime(millisec) { + const ms = Math.floor(millisec % 1000); + const secs = Math.floor((millisec / 1000) % 60); + const mins = Math.floor((millisec / (1000 * 60)) % 60); + const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24); + const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7); + const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4); + const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12); + const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)); + + let formattedTime = '0 milliseconds'; if (ms > 0) { - formattedTime = `${ms} milliseconds` + formattedTime = `${ms} milliseconds`; } if (secs > 0) { - formattedTime = `${secs} seconds ${formattedTime}` + formattedTime = `${secs} seconds ${formattedTime}`; } if (mins > 0) { - formattedTime = `${mins} minutes ${formattedTime}` + formattedTime = `${mins} minutes ${formattedTime}`; } if (hrs > 0) { - formattedTime = `${hrs} hours ${formattedTime}` + formattedTime = `${hrs} hours ${formattedTime}`; } if (days > 0) { - formattedTime = `${days} days ${formattedTime}` + formattedTime = `${days} days ${formattedTime}`; } if (weeks > 0) { - formattedTime = `${weeks} weeks ${formattedTime}` + formattedTime = `${weeks} weeks ${formattedTime}`; } if (mnths > 0) { - formattedTime = `${mnths} months ${formattedTime}` + formattedTime = `${mnths} months ${formattedTime}`; } if (yrs > 0) { - formattedTime = `${yrs} years ${formattedTime}` + formattedTime = `${yrs} years ${formattedTime}`; } - return formattedTime.trim() + return formattedTime.trim(); } /** @@ -523,30 +647,30 @@ function formatTime (millisec) { * @param {Array} source the array in which to search for the term * @param {Array | String} term the term to search */ -function checkIfExists (source, term) { - let terms +function checkIfExists(source, term) { + let terms; if (!_.isArray(source)) { - throw new Error('Source argument should be an array') + throw new Error('Source argument should be an array'); } - source = source.map(s => s.toLowerCase()) + source = source.map((s) => s.toLowerCase()); if (_.isString(term)) { - terms = term.toLowerCase().split(' ') + terms = term.toLowerCase().split(' '); } else if (_.isArray(term)) { - terms = term.map(t => t.toLowerCase()) + terms = term.map((t) => t.toLowerCase()); } else { - throw new Error('Term argument should be either a string or an array') + throw new Error('Term argument should be either a string or an array'); } for (let i = 0; i < terms.length; i++) { if (source.includes(terms[i])) { - return true + return true; } } - return false + return false; } /** @@ -554,10 +678,10 @@ function checkIfExists (source, term) { * @param {Function} fn the async function * @returns {Function} the wrapped function */ -function wrapExpress (fn) { +function wrapExpress(fn) { return function (req, res, next) { - fn(req, res, next).catch(next) - } + fn(req, res, next).catch(next); + }; } /** @@ -565,20 +689,20 @@ function wrapExpress (fn) { * @param obj the object (controller exports) * @returns {Object|Array} the wrapped object */ -function autoWrapExpress (obj) { +function autoWrapExpress(obj) { if (_.isArray(obj)) { - return obj.map(autoWrapExpress) + return obj.map(autoWrapExpress); } if (_.isFunction(obj)) { if (obj.constructor.name === 'AsyncFunction') { - return wrapExpress(obj) + return wrapExpress(obj); } - return obj + return obj; } _.each(obj, (value, key) => { - obj[key] = autoWrapExpress(value) - }) - return obj + obj[key] = autoWrapExpress(value); + }); + return obj; } /** @@ -587,9 +711,11 @@ function autoWrapExpress (obj) { * @param {Number} page the page number * @returns {String} link for the page */ -function getPageLink (req, page) { - const q = _.assignIn({}, req.query, { page }) - return `${req.protocol}://${req.get('Host')}${req.baseUrl}${req.path}?${querystring.stringify(q)}` +function getPageLink(req, page) { + const q = _.assignIn({}, req.query, { page }); + return `${req.protocol}://${req.get('Host')}${req.baseUrl}${ + req.path + }?${querystring.stringify(q)}`; } /** @@ -598,31 +724,34 @@ function getPageLink (req, page) { * @param {Object} res the HTTP response * @param {Object} result the operation result */ -function setResHeaders (req, res, result) { +function setResHeaders(req, res, result) { if (result.fromDb) { - return + return; } - const totalPages = Math.ceil(result.total / result.perPage) + const totalPages = Math.ceil(result.total / result.perPage); if (result.page > 1) { - res.set('X-Prev-Page', result.page - 1) + res.set('X-Prev-Page', result.page - 1); } if (result.page < totalPages) { - res.set('X-Next-Page', result.page + 1) + res.set('X-Next-Page', result.page + 1); } - res.set('X-Page', result.page) - res.set('X-Per-Page', result.perPage) - res.set('X-Total', result.total) - res.set('X-Total-Pages', totalPages) + res.set('X-Page', result.page); + res.set('X-Per-Page', result.perPage); + res.set('X-Total', result.total); + res.set('X-Total-Pages', totalPages); // set Link header if (totalPages > 0) { - let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink(req, totalPages)}>; rel="last"` + let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink( + req, + totalPages + )}>; rel="last"`; if (result.page > 1) { - link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"` + link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"`; } if (result.page < totalPages) { - link += `, <${getPageLink(req, result.page + 1)}>; rel="next"` + link += `, <${getPageLink(req, result.page + 1)}>; rel="next"`; } - res.set('Link', link) + res.set('Link', link); } } @@ -630,30 +759,30 @@ function setResHeaders (req, res, result) { * Get ES Client * @return {Object} Elastic Host Client Instance */ -function getESClient () { +function getESClient() { if (esClients.client) { - return esClients.client + return esClients.client; } - const host = config.esConfig.HOST - const cloudId = config.esConfig.ELASTICCLOUD.id + const host = config.esConfig.HOST; + const cloudId = config.esConfig.ELASTICCLOUD.id; if (cloudId) { // Elastic Cloud configuration esClients.client = new elasticsearch.Client({ cloud: { - id: cloudId + id: cloudId, }, auth: { username: config.esConfig.ELASTICCLOUD.username, - password: config.esConfig.ELASTICCLOUD.password - } - }) + password: config.esConfig.ELASTICCLOUD.password, + }, + }); } else { esClients.client = new elasticsearch.Client({ - node: host - }) + node: host, + }); } - return esClients.client + return esClients.client; } /* @@ -661,16 +790,22 @@ function getESClient () { * @returns {Promise} */ const getM2MToken = async () => { - return await m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) -} + return await m2m.getMachineToken( + config.AUTH0_CLIENT_ID, + config.AUTH0_CLIENT_SECRET + ); +}; /* * Function to get M2M token for U-Bahn * @returns {Promise} */ const getM2MUbahnToken = async () => { - return await m2mForUbahn.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) -} + return await m2mForUbahn.getMachineToken( + config.AUTH0_CLIENT_ID, + config.AUTH0_CLIENT_SECRET + ); +}; /** * Function to encode query string @@ -678,17 +813,17 @@ const getM2MUbahnToken = async () => { * @param {String} nesting the nesting string * @returns {String} query string */ -function encodeQueryString (queryObj, nesting = '') { +function encodeQueryString(queryObj, nesting = '') { const pairs = Object.entries(queryObj).map(([key, val]) => { // Handle the nested, recursive case, where the value to encode is an object itself if (typeof val === 'object') { - return encodeQueryString(val, nesting + `${key}.`) + return encodeQueryString(val, nesting + `${key}.`); } else { // Handle base case, where the value to encode is simply a string. - return [nesting + key, val].map(querystring.escape).join('=') + return [nesting + key, val].map(querystring.escape).join('='); } - }) - return pairs.join('&') + }); + return pairs.join('&'); } /** @@ -696,28 +831,31 @@ function encodeQueryString (queryObj, nesting = '') { * @param {Integer} externalId the legacy user id * @returns {Array} the users found */ -async function listUsersByExternalId (externalId) { +async function listUsersByExternalId(externalId) { // return empty list if externalId is null or undefined if (!!externalId !== true) { - return [] + return []; } - const token = await getM2MUbahnToken() + const token = await getM2MUbahnToken(); const q = { enrich: true, externalProfile: { organizationId: config.ORG_ID, - externalId - } - } - const url = `${config.TC_API}/users?${encodeQueryString(q)}` + externalId, + }, + }; + const url = `${config.TC_API}/users?${encodeQueryString(q)}`; const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'listUserByExternalId', message: `response body: ${JSON.stringify(res.body)}` }) - return res.body + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'listUserByExternalId', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return res.body; } /** @@ -725,12 +863,14 @@ async function listUsersByExternalId (externalId) { * @param {Integer} externalId the legacy user id * @returns {Object} the user */ -async function getUserByExternalId (externalId) { - const users = await listUsersByExternalId(externalId) +async function getUserByExternalId(externalId) { + const users = await listUsersByExternalId(externalId); if (_.isEmpty(users)) { - throw new errors.NotFoundError(`externalId: ${externalId} "user" not found`) + throw new errors.NotFoundError( + `externalId: ${externalId} "user" not found` + ); } - return users[0] + return users[0]; } /** @@ -739,18 +879,24 @@ async function getUserByExternalId (externalId) { * @params {Object} payload the payload * @params {Object} options the extra options to control the function */ -async function postEvent (topic, payload, options = {}) { - logger.debug({ component: 'helper', context: 'postEvent', message: `Posting event to Kafka topic ${topic}, ${JSON.stringify(payload)}` }) - const client = getBusApiClient() +async function postEvent(topic, payload, options = {}) { + logger.debug({ + component: 'helper', + context: 'postEvent', + message: `Posting event to Kafka topic ${topic}, ${JSON.stringify( + payload + )}`, + }); + const client = getBusApiClient(); const message = { topic, originator: config.KAFKA_MESSAGE_ORIGINATOR, timestamp: new Date().toISOString(), 'mime-type': 'application/json', - payload - } - await client.postEvent(message) - await eventDispatcher.handleEvent(topic, { value: payload, options }) + payload, + }; + await client.postEvent(message); + await eventDispatcher.handleEvent(topic, { value: payload, options }); } /** @@ -759,11 +905,11 @@ async function postEvent (topic, payload, options = {}) { * @param {Object} err the err * @returns {Boolean} the result */ -function isDocumentMissingException (err) { +function isDocumentMissingException(err) { if (err.statusCode === 404 && err instanceof ESResponseError) { - return true + return true; } - return false + return false; } /** @@ -772,31 +918,34 @@ function isDocumentMissingException (err) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getProjects (currentUser, criteria = {}) { - let token +async function getProjects(currentUser, criteria = {}) { + let token; if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken() - token = `Bearer ${m2mToken}` + const m2mToken = await getM2MToken(); + token = `Bearer ${m2mToken}`; } else { - token = currentUser.jwtToken + token = currentUser.jwtToken; } - const url = `${config.TC_API}/projects?type=talent-as-a-service` + const url = `${config.TC_API}/projects?type=talent-as-a-service`; const res = await request .get(url) .query(criteria) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'getProjects', message: `response body: ${JSON.stringify(res.body)}` }) - const result = _.map(res.body, item => { - return _.pick(item, ['id', 'name', 'invites', 'members']) - }) + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getProjects', + message: `response body: ${JSON.stringify(res.body)}`, + }); + const result = _.map(res.body, (item) => { + return _.pick(item, ['id', 'name', 'invites', 'members']); + }); return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result - } + result, + }; } /** @@ -805,19 +954,24 @@ async function getProjects (currentUser, criteria = {}) { * @param {String} userId the legacy user id * @returns {Object} the user */ -async function getTopcoderUserById (userId) { - const token = await getM2MToken() +async function getTopcoderUserById(userId) { + const token = await getM2MToken(); const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `id=${userId}` }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json') - localLogger.debug({ context: 'getTopcoderUserById', message: `response body: ${JSON.stringify(res.body)}` }) - const user = _.get(res.body, 'result.content[0]') + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getTopcoderUserById', + message: `response body: ${JSON.stringify(res.body)}`, + }); + const user = _.get(res.body, 'result.content[0]'); if (!user) { - throw new errors.NotFoundError(`userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}`) + throw new errors.NotFoundError( + `userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}` + ); } - return user + return user; } /** @@ -825,24 +979,31 @@ async function getTopcoderUserById (userId) { * @param {String} userId the user id * @returns the request result */ -async function getUserById (userId, enrich) { - const token = await getM2MUbahnToken() +async function getUserById(userId, enrich) { + const token = await getM2MUbahnToken(); const res = await request .get(`${config.TC_API}/users/${userId}` + (enrich ? '?enrich=true' : '')) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'getUserById', message: `response body: ${JSON.stringify(res.body)}` }) + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getUserById', + message: `response body: ${JSON.stringify(res.body)}`, + }); - const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']) + const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']); if (enrich) { - user.skills = (res.body.skills || []).map((skillObj) => _.pick(skillObj.skill, ['id', 'name'])) - const attributes = _.get(res, 'body.attributes', []) - user.attributes = _.map(attributes, attr => _.pick(attr, ['id', 'value', 'attribute.id', 'attribute.name'])) + user.skills = (res.body.skills || []).map((skillObj) => + _.pick(skillObj.skill, ['id', 'name']) + ); + const attributes = _.get(res, 'body.attributes', []); + user.attributes = _.map(attributes, (attr) => + _.pick(attr, ['id', 'value', 'attribute.id', 'attribute.name']) + ); } - return user + return user; } /** @@ -850,16 +1011,19 @@ async function getUserById (userId, enrich) { * @param {Object} data the user data * @returns the request result */ -async function createUbahnUser ({ handle, firstName, lastName }) { - const token = await getM2MUbahnToken() +async function createUbahnUser({ handle, firstName, lastName }) { + const token = await getM2MUbahnToken(); const res = await request .post(`${config.TC_API}/users`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ handle, firstName, lastName }) - localLogger.debug({ context: 'createUbahnUser', message: `response body: ${JSON.stringify(res.body)}` }) - return _.pick(res.body, ['id']) + .send({ handle, firstName, lastName }); + localLogger.debug({ + context: 'createUbahnUser', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.pick(res.body, ['id']); } /** @@ -867,15 +1031,21 @@ async function createUbahnUser ({ handle, firstName, lastName }) { * @param {String} userId the user id(with uuid format) * @param {Object} data the profile data */ -async function createUserExternalProfile (userId, { organizationId, externalId }) { - const token = await getM2MUbahnToken() +async function createUserExternalProfile( + userId, + { organizationId, externalId } +) { + const token = await getM2MUbahnToken(); const res = await request .post(`${config.TC_API}/users/${userId}/externalProfiles`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ organizationId, externalId: String(externalId) }) - localLogger.debug({ context: 'createUserExternalProfile', message: `response body: ${JSON.stringify(res.body)}` }) + .send({ organizationId, externalId: String(externalId) }); + localLogger.debug({ + context: 'createUserExternalProfile', + message: `response body: ${JSON.stringify(res.body)}`, + }); } /** @@ -883,20 +1053,23 @@ async function createUserExternalProfile (userId, { organizationId, externalId } * @param {Array} handles the handle array * @returns the request result */ -async function getMembers (handles) { - const token = await getM2MToken() - const handlesStr = _.map(handles, handle => { - return '%22' + handle.toLowerCase() + '%22' - }).join(',') - const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]` +async function getMembers(handles) { + const token = await getM2MToken(); + const handlesStr = _.map(handles, (handle) => { + return '%22' + handle.toLowerCase() + '%22'; + }).join(','); + const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]`; const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'getMembers', message: `response body: ${JSON.stringify(res.body)}` }) - return res.body + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getMembers', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return res.body; } /** @@ -905,31 +1078,36 @@ async function getMembers (handles) { * @param {Number} id project id * @returns the request result */ -async function getProjectById (currentUser, id) { - let token +async function getProjectById(currentUser, id) { + let token; if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken() - token = `Bearer ${m2mToken}` + const m2mToken = await getM2MToken(); + token = `Bearer ${m2mToken}`; } else { - token = currentUser.jwtToken + token = currentUser.jwtToken; } - const url = `${config.TC_API}/projects/${id}` + const url = `${config.TC_API}/projects/${id}`; try { const res = await request .get(url) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'getProjectById', message: `response body: ${JSON.stringify(res.body)}` }) - return _.pick(res.body, ['id', 'name', 'invites', 'members']) + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getProjectById', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.pick(res.body, ['id', 'name', 'invites', 'members']); } catch (err) { if (err.status === HttpStatus.FORBIDDEN) { - throw new errors.ForbiddenError(`You are not allowed to access the project with id ${id}`) + throw new errors.ForbiddenError( + `You are not allowed to access the project with id ${id}` + ); } if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${id} project not found`) + throw new errors.NotFoundError(`id: ${id} project not found`); } - throw err + throw err; } } @@ -940,30 +1118,33 @@ async function getProjectById (currentUser, id) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getTopcoderSkills (criteria) { - const token = await getM2MUbahnToken() +async function getTopcoderSkills(criteria) { + const token = await getM2MUbahnToken(); try { const res = await request .get(`${config.TC_API}/skills`) .query({ skillProviderId: config.TOPCODER_SKILL_PROVIDER_ID, - ...criteria + ...criteria, }) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'getTopcoderSkills', message: `response body: ${JSON.stringify(res.body)}` }) + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getTopcoderSkills', + message: `response body: ${JSON.stringify(res.body)}`, + }); return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result: res.body - } + result: res.body, + }; } catch (err) { if (err.status === HttpStatus.BAD_REQUEST) { - throw new errors.BadRequestError(err.response.body.message) + throw new errors.BadRequestError(err.response.body.message); } - throw err + throw err; } } @@ -972,15 +1153,18 @@ async function getTopcoderSkills (criteria) { * @param {String} skillId the skill Id * @returns the request result */ -async function getSkillById (skillId) { - const token = await getM2MUbahnToken() +async function getSkillById(skillId) { + const token = await getM2MUbahnToken(); const res = await request .get(`${config.TC_API}/skills/${skillId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'getSkillById', message: `response body: ${JSON.stringify(res.body)}` }) - return _.pick(res.body, ['id', 'name']) + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getSkillById', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.pick(res.body, ['id', 'name']); } /** @@ -993,17 +1177,22 @@ async function getSkillById (skillId) { * @params {Object} currentUser the user who perform this operation * @returns {String} the ubahn user id */ -async function ensureUbahnUserId (currentUser) { +async function ensureUbahnUserId(currentUser) { try { - return (await getUserByExternalId(currentUser.userId)).id + return (await getUserByExternalId(currentUser.userId)).id; } catch (err) { if (!(err instanceof errors.NotFoundError)) { - throw err + throw err; } - const topcoderUser = await getTopcoderUserById(currentUser.userId) - const user = await createUbahnUser(_.pick(topcoderUser, ['handle', 'firstName', 'lastName'])) - await createUserExternalProfile(user.id, { organizationId: config.ORG_ID, externalId: currentUser.userId }) - return user.id + const topcoderUser = await getTopcoderUserById(currentUser.userId); + const user = await createUbahnUser( + _.pick(topcoderUser, ['handle', 'firstName', 'lastName']) + ); + await createUserExternalProfile(user.id, { + organizationId: config.ORG_ID, + externalId: currentUser.userId, + }); + return user.id; } } @@ -1013,8 +1202,8 @@ async function ensureUbahnUserId (currentUser) { * @param {String} jobId the job id * @returns {Object} the job data */ -async function ensureJobById (jobId) { - return models.Job.findById(jobId) +async function ensureJobById(jobId) { + return models.Job.findById(jobId); } /** @@ -1023,8 +1212,8 @@ async function ensureJobById (jobId) { * @param {String} resourceBookingId the resourceBooking id * @returns {Object} the resourceBooking data */ -async function ensureResourceBookingById (resourceBookingId) { - return models.ResourceBooking.findById(resourceBookingId) +async function ensureResourceBookingById(resourceBookingId) { + return models.ResourceBooking.findById(resourceBookingId); } /** @@ -1032,8 +1221,8 @@ async function ensureResourceBookingById (resourceBookingId) { * @param {String} workPeriodId the workPeriod id * @returns the workPeriod data */ -async function ensureWorkPeriodById (workPeriodId) { - return models.WorkPeriod.findById(workPeriodId) +async function ensureWorkPeriodById(workPeriodId) { + return models.WorkPeriod.findById(workPeriodId); } /** @@ -1042,21 +1231,24 @@ async function ensureWorkPeriodById (workPeriodId) { * @param {String} jobId the user id * @returns {Object} the user data */ -async function ensureUserById (userId) { - const token = await getM2MUbahnToken() +async function ensureUserById(userId) { + const token = await getM2MUbahnToken(); try { const res = await request .get(`${config.TC_API}/users/${userId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'ensureUserById', message: `response body: ${JSON.stringify(res.body)}` }) - return res.body + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'ensureUserById', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return res.body; } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${userId} "user" not found`) + throw new errors.NotFoundError(`id: ${userId} "user" not found`); } - throw err + throw err; } } @@ -1065,8 +1257,12 @@ async function ensureUserById (userId) { * * @returns {Object} the M2M auth user */ -function getAuditM2Muser () { - return { isMachine: true, userId: config.m2m.M2M_AUDIT_USER_ID, handle: config.m2m.M2M_AUDIT_HANDLE } +function getAuditM2Muser() { + return { + isMachine: true, + userId: config.m2m.M2M_AUDIT_USER_ID, + handle: config.m2m.M2M_AUDIT_HANDLE, + }; } /** @@ -1078,17 +1274,24 @@ function getAuditM2Muser () { * @param {Number} projectId project id * @returns the result */ -async function checkIsMemberOfProject (userId, projectId) { - const m2mToken = await getM2MToken() +async function checkIsMemberOfProject(userId, projectId) { + const m2mToken = await getM2MToken(); const res = await request .get(`${config.TC_API}/projects/${projectId}`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - const memberIdList = _.map(res.body.members, 'userId') - localLogger.debug({ context: 'checkIsMemberOfProject', message: `the members of project ${projectId}: ${JSON.stringify(memberIdList)}, authUserId: ${JSON.stringify(userId)}` }) + .set('Accept', 'application/json'); + const memberIdList = _.map(res.body.members, 'userId'); + localLogger.debug({ + context: 'checkIsMemberOfProject', + message: `the members of project ${projectId}: ${JSON.stringify( + memberIdList + )}, authUserId: ${JSON.stringify(userId)}`, + }); if (!memberIdList.includes(userId)) { - throw new errors.UnauthorizedError(`userId: ${userId} the user is not a member of project ${projectId}`) + throw new errors.UnauthorizedError( + `userId: ${userId} the user is not a member of project ${projectId}` + ); } } @@ -1098,21 +1301,27 @@ async function checkIsMemberOfProject (userId, projectId) { * @param {Array} handles the array of handles * @returns {Array} the member details */ -async function getMemberDetailsByHandles (handles) { +async function getMemberDetailsByHandles(handles) { if (!handles.length) { - return [] + return []; } - const token = await getM2MToken() + const token = await getM2MToken(); const res = await request .get(`${config.TOPCODER_MEMBERS_API}/_search`) .query({ - query: _.map(handles, handle => `handleLower:${handle.toLowerCase()}`).join(' OR '), - fields: 'userId,handle,firstName,lastName,email' + query: _.map( + handles, + (handle) => `handleLower:${handle.toLowerCase()}` + ).join(' OR '), + fields: 'userId,handle,firstName,lastName,email', }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json') - localLogger.debug({ context: 'getMemberDetailsByHandles', message: `response body: ${JSON.stringify(res.body)}` }) - return _.get(res.body, 'result.content') + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getMemberDetailsByHandles', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.get(res.body, 'result.content'); } /** @@ -1121,14 +1330,17 @@ async function getMemberDetailsByHandles (handles) { * @param {String} handle the user handle * @returns {Object} the member details */ -async function getV3MemberDetailsByHandle (handle) { - const token = await getM2MToken() +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') + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getV3MemberDetailsByHandle', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.get(res.body, 'result.content'); } /** @@ -1138,17 +1350,20 @@ async function getV3MemberDetailsByHandle (handle) { * @param {String} email the email * @returns {Array} the member details */ -async function _getMemberDetailsByEmail (token, email) { +async function _getMemberDetailsByEmail(token, email) { const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `email=${email}`, - fields: 'handle,id,email,firstName,lastName' + fields: 'handle,id,email,firstName,lastName', }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json') - localLogger.debug({ context: '_getMemberDetailsByEmail', message: `response body: ${JSON.stringify(res.body)}` }) - return _.get(res.body, 'result.content') + .set('Accept', 'application/json'); + localLogger.debug({ + context: '_getMemberDetailsByEmail', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.get(res.body, 'result.content'); } /** @@ -1158,16 +1373,25 @@ async function _getMemberDetailsByEmail (token, email) { * @param {Array} emails the array of emails * @returns {Array} the member details */ -async function getMemberDetailsByEmails (emails) { - const token = await getM2MToken() - const limiter = new Bottleneck({ maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API }) - const membersArray = await Promise.all(emails.map(email => limiter.schedule(() => _getMemberDetailsByEmail(token, email) - .catch((error) => { - localLogger.error({ context: 'getMemberDetailsByEmails', message: error.message }) - return [] - }) - ))) - return _.flatten(membersArray) +async function getMemberDetailsByEmails(emails) { + const token = await getM2MToken(); + const limiter = new Bottleneck({ + maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API, + }); + const membersArray = await Promise.all( + emails.map((email) => + limiter.schedule(() => + _getMemberDetailsByEmail(token, email).catch((error) => { + localLogger.error({ + context: 'getMemberDetailsByEmails', + message: error.message, + }); + return []; + }) + ) + ) + ); + return _.flatten(membersArray); } /** @@ -1178,17 +1402,20 @@ async function getMemberDetailsByEmails (emails) { * @param {Object} criteria the filtering criteria * @returns {Object} the member created */ -async function createProjectMember (projectId, data, criteria) { - const m2mToken = await getM2MToken() +async function createProjectMember(projectId, data, criteria) { + const m2mToken = await getM2MToken(); const { body: member } = await request .post(`${config.TC_API}/projects/${projectId}/members`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') .query(criteria) - .send(data) - localLogger.debug({ context: 'createProjectMember', message: `response body: ${JSON.stringify(member)}` }) - return member + .send(data); + localLogger.debug({ + context: 'createProjectMember', + message: `response body: ${JSON.stringify(member)}`, + }); + return member; } /** @@ -1198,17 +1425,21 @@ async function createProjectMember (projectId, data, criteria) { * @param {Object} criteria the search criteria * @returns {Array} the project members */ -async function listProjectMembers (currentUser, projectId, criteria = {}) { - const token = (currentUser.hasManagePermission || currentUser.isMachine) - ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken +async function listProjectMembers(currentUser, projectId, criteria = {}) { + const token = + currentUser.hasManagePermission || currentUser.isMachine + ? `Bearer ${await getM2MToken()}` + : currentUser.jwtToken; const { body: members } = await request .get(`${config.TC_API}/projects/${projectId}/members`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json') - localLogger.debug({ context: 'listProjectMembers', message: `response body: ${JSON.stringify(members)}` }) - return members + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'listProjectMembers', + message: `response body: ${JSON.stringify(members)}`, + }); + return members; } /** @@ -1218,17 +1449,21 @@ async function listProjectMembers (currentUser, projectId, criteria = {}) { * @param {Object} criteria the search criteria * @returns {Array} the member invites */ -async function listProjectMemberInvites (currentUser, projectId, criteria = {}) { - const token = (currentUser.hasManagePermission || currentUser.isMachine) - ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken +async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { + const token = + currentUser.hasManagePermission || currentUser.isMachine + ? `Bearer ${await getM2MToken()}` + : currentUser.jwtToken; const { body: invites } = await request .get(`${config.TC_API}/projects/${projectId}/invites`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json') - localLogger.debug({ context: 'listProjectMemberInvites', message: `response body: ${JSON.stringify(invites)}` }) - return invites + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'listProjectMemberInvites', + message: `response body: ${JSON.stringify(invites)}`, + }); + return invites; } /** @@ -1238,19 +1473,24 @@ async function listProjectMemberInvites (currentUser, projectId, criteria = {}) * @param {String} projectMemberId the id of the project member * @returns {undefined} */ -async function deleteProjectMember (currentUser, projectId, projectMemberId) { - const token = (currentUser.hasManagePermission || currentUser.isMachine) - ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken +async function deleteProjectMember(currentUser, projectId, projectMemberId) { + const token = + currentUser.hasManagePermission || currentUser.isMachine + ? `Bearer ${await getM2MToken()}` + : currentUser.jwtToken; try { await request - .delete(`${config.TC_API}/projects/${projectId}/members/${projectMemberId}`) - .set('Authorization', token) + .delete( + `${config.TC_API}/projects/${projectId}/members/${projectMemberId}` + ) + .set('Authorization', token); } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}`) + throw new errors.NotFoundError( + `projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}` + ); } - throw err + throw err; } } @@ -1260,10 +1500,13 @@ async function deleteProjectMember (currentUser, projectId, projectMemberId) { * @param {String} attributeName Requested attribute name, e.g. "email" * @returns attribute value */ -function getUserAttributeValue (user, attributeName) { - const attributes = _.get(user, 'attributes', []) - const targetAttribute = _.find(attributes, a => a.attribute.name === attributeName) - return _.get(targetAttribute, 'value') +function getUserAttributeValue(user, attributeName) { + const attributes = _.get(user, 'attributes', []); + const targetAttribute = _.find( + attributes, + (a) => a.attribute.name === attributeName + ); + return _.get(targetAttribute, 'value'); } /** @@ -1273,22 +1516,34 @@ function getUserAttributeValue (user, attributeName) { * @param {String} token m2m token * @returns {Object} the challenge created */ -async function createChallenge (data, token) { +async function createChallenge(data, token) { if (!token) { - token = await getM2MToken() + token = await getM2MToken(); } - const url = `${config.TC_API}/challenges` - localLogger.debug({ context: 'createChallenge', message: `EndPoint: POST ${url}` }) - localLogger.debug({ context: 'createChallenge', message: `Request Body: ${JSON.stringify(data)}` }) + const url = `${config.TC_API}/challenges`; + localLogger.debug({ + context: 'createChallenge', + message: `EndPoint: POST ${url}`, + }); + localLogger.debug({ + context: 'createChallenge', + message: `Request Body: ${JSON.stringify(data)}`, + }); const { body: challenge, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data) - localLogger.debug({ context: 'createChallenge', message: `Status Code: ${httpStatus}` }) - localLogger.debug({ context: 'createChallenge', message: `Response Body: ${JSON.stringify(challenge)}` }) - return challenge + .send(data); + localLogger.debug({ + context: 'createChallenge', + message: `Status Code: ${httpStatus}`, + }); + localLogger.debug({ + context: 'createChallenge', + message: `Response Body: ${JSON.stringify(challenge)}`, + }); + return challenge; } /** @@ -1299,22 +1554,34 @@ async function createChallenge (data, token) { * @param {String} token m2m token * @returns {Object} the challenge updated */ -async function updateChallenge (challengeId, data, token) { +async function updateChallenge(challengeId, data, token) { if (!token) { - token = await getM2MToken() + token = await getM2MToken(); } - const url = `${config.TC_API}/challenges/${challengeId}` - localLogger.debug({ context: 'updateChallenge', message: `EndPoint: PATCH ${url}` }) - localLogger.debug({ context: 'updateChallenge', message: `Request Body: ${JSON.stringify(data)}` }) + const url = `${config.TC_API}/challenges/${challengeId}`; + localLogger.debug({ + context: 'updateChallenge', + message: `EndPoint: PATCH ${url}`, + }); + localLogger.debug({ + context: 'updateChallenge', + message: `Request Body: ${JSON.stringify(data)}`, + }); const { body: challenge, status: httpStatus } = await request .patch(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data) - localLogger.debug({ context: 'updateChallenge', message: `Status Code: ${httpStatus}` }) - localLogger.debug({ context: 'updateChallenge', message: `Response Body: ${JSON.stringify(challenge)}` }) - return challenge + .send(data); + localLogger.debug({ + context: 'updateChallenge', + message: `Status Code: ${httpStatus}`, + }); + localLogger.debug({ + context: 'updateChallenge', + message: `Response Body: ${JSON.stringify(challenge)}`, + }); + return challenge; } /** @@ -1324,22 +1591,34 @@ async function updateChallenge (challengeId, data, token) { * @param {String} token m2m token * @returns {Object} the resource created */ -async function createChallengeResource (data, token) { +async function createChallengeResource(data, token) { if (!token) { - token = await getM2MToken() + token = await getM2MToken(); } - const url = `${config.TC_API}/resources` - localLogger.debug({ context: 'createChallengeResource', message: `EndPoint: POST ${url}` }) - localLogger.debug({ context: 'createChallengeResource', message: `Request Body: ${JSON.stringify(data)}` }) + const url = `${config.TC_API}/resources`; + localLogger.debug({ + context: 'createChallengeResource', + message: `EndPoint: POST ${url}`, + }); + localLogger.debug({ + context: 'createChallengeResource', + message: `Request Body: ${JSON.stringify(data)}`, + }); const { body: resource, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data) - localLogger.debug({ context: 'createChallengeResource', message: `Status Code: ${httpStatus}` }) - localLogger.debug({ context: 'createChallengeResource', message: `Response Body: ${JSON.stringify(resource)}` }) - return resource + .send(data); + localLogger.debug({ + context: 'createChallengeResource', + message: `Status Code: ${httpStatus}`, + }); + localLogger.debug({ + context: 'createChallengeResource', + message: `Response Body: ${JSON.stringify(resource)}`, + }); + return resource; } /** @@ -1348,40 +1627,40 @@ async function createChallengeResource (data, token) { * @param {Date} end end date of the resource booking * @returns {Array<{startDate:Date, endDate:Date, daysWorked:number}>} information about workPeriods */ -function extractWorkPeriods (start, end) { +function extractWorkPeriods(start, end) { // calculate maximum possible daysWorked for a week - function getDaysWorked (week) { + function getDaysWorked(week) { if (weeks === 1) { - return Math.min(endDay, 5) - Math.max(startDay, 1) + 1 + return Math.min(endDay, 5) - Math.max(startDay, 1) + 1; } else if (week === 0) { - return Math.min(6 - startDay, 5) - } else if (week === (weeks - 1)) { - return Math.min(endDay, 5) - } else return 5 + return Math.min(6 - startDay, 5); + } else if (week === weeks - 1) { + return Math.min(endDay, 5); + } else return 5; } - const periods = [] + const periods = []; if (_.isNil(start) || _.isNil(end)) { - return periods + return periods; } - const startDate = moment(start) - const startDay = startDate.get('day') - startDate.set('day', 0).startOf('day') + const startDate = moment(start); + const startDay = startDate.get('day'); + startDate.set('day', 0).startOf('day'); - const endDate = moment(end) - const endDay = endDate.get('day') - endDate.set('day', 6).endOf('day') + const endDate = moment(end); + const endDay = endDate.get('day'); + endDate.set('day', 6).endOf('day'); - const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7 + const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7; for (let i = 0; i < weeks; i++) { periods.push({ startDate: startDate.format('YYYY-MM-DD'), endDate: startDate.add(6, 'day').format('YYYY-MM-DD'), - daysWorked: getDaysWorked(i) - }) - startDate.add(1, 'day') + daysWorked: getDaysWorked(i), + }); + startDate.add(1, 'day'); } - return periods + return periods; } /** @@ -1390,16 +1669,19 @@ function extractWorkPeriods (start, end) { * @param {String} userHandle user handle * @returns {String} email address of the user */ -async function getUserByHandle (userHandle) { - const token = await getM2MToken() - const url = `${config.TC_API}/members/${userHandle}` +async function getUserByHandle(userHandle) { + const token = await getM2MToken(); + const url = `${config.TC_API}/members/${userHandle}`; const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - localLogger.debug({ context: 'getUserByHandle', message: `response body: ${JSON.stringify(res.body)}` }) - return _.get(res, 'body') + .set('Accept', 'application/json'); + localLogger.debug({ + context: 'getUserByHandle', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.get(res, 'body'); } /** @@ -1408,14 +1690,34 @@ async function getUserByHandle (userHandle) { * @param {*} object of json that would be replaced in string * @returns */ -async function substituteStringByObject (string, object) { +async function substituteStringByObject(string, object) { for (var key in object) { if (!Object.prototype.hasOwnProperty.call(object, key)) { - continue + continue; } - string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]) + string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]); } - return string + return string; +} + +/** + * @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; + const res = await request + .post(`${config.TC_API}/projects/`) + .set('Authorization', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .send(data); + localLogger.debug({ + context: 'createProject', + message: `response body: ${JSON.stringify(res)}`, + }); + return _.get(res, 'body'); } module.exports = { @@ -1434,9 +1736,9 @@ module.exports = { getUserId: async (userId) => { // check m2m user id if (userId === config.m2m.M2M_AUDIT_USER_ID) { - return config.m2m.M2M_AUDIT_USER_ID + return config.m2m.M2M_AUDIT_USER_ID; } - return ensureUbahnUserId({ userId }) + return ensureUbahnUserId({ userId }); }, getUserByExternalId, getM2MToken, @@ -1469,5 +1771,6 @@ module.exports = { createChallengeResource, extractWorkPeriods, getUserByHandle, - substituteStringByObject -} + substituteStringByObject, + createProject, +}; diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index b8f7c149..ca4f1bca 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -1,19 +1,19 @@ /** * Controller for TaaS teams endpoints */ -const HttpStatus = require('http-status-codes') -const service = require('../services/TeamService') -const helper = require('../common/helper') +const HttpStatus = require('http-status-codes'); +const service = require('../services/TeamService'); +const helper = require('../common/helper'); /** * Search teams * @param req the request * @param res the response */ -async function searchTeams (req, res) { - const result = await service.searchTeams(req.authUser, req.query) - helper.setResHeaders(req, res, result) - res.send(result.result) +async function searchTeams(req, res) { + const result = await service.searchTeams(req.authUser, req.query); + helper.setResHeaders(req, res, result); + res.send(result.result); } /** @@ -21,8 +21,8 @@ async function searchTeams (req, res) { * @param req the request * @param res the response */ -async function getTeam (req, res) { - res.send(await service.getTeam(req.authUser, req.params.id)) +async function getTeam(req, res) { + res.send(await service.getTeam(req.authUser, req.params.id)); } /** @@ -30,8 +30,10 @@ async function getTeam (req, res) { * @param req the request * @param res the response */ -async function getTeamJob (req, res) { - res.send(await service.getTeamJob(req.authUser, req.params.id, req.params.jobId)) +async function getTeamJob(req, res) { + res.send( + await service.getTeamJob(req.authUser, req.params.id, req.params.jobId) + ); } /** @@ -39,9 +41,9 @@ async function getTeamJob (req, res) { * @param req the request * @param res the response */ -async function sendEmail (req, res) { - await service.sendEmail(req.authUser, req.body) - res.status(HttpStatus.NO_CONTENT).end() +async function sendEmail(req, res) { + await service.sendEmail(req.authUser, req.body); + res.status(HttpStatus.NO_CONTENT).end(); } /** @@ -49,8 +51,10 @@ async function sendEmail (req, res) { * @param req the request * @param res the response */ -async function addMembers (req, res) { - res.send(await service.addMembers(req.authUser, req.params.id, req.query, req.body)) +async function addMembers(req, res) { + res.send( + await service.addMembers(req.authUser, req.params.id, req.query, req.body) + ); } /** @@ -58,9 +62,13 @@ async function addMembers (req, res) { * @param req the request * @param res the response */ -async function searchMembers (req, res) { - const result = await service.searchMembers(req.authUser, req.params.id, req.query) - res.send(result.result) +async function searchMembers(req, res) { + const result = await service.searchMembers( + req.authUser, + req.params.id, + req.query + ); + res.send(result.result); } /** @@ -68,9 +76,13 @@ async function searchMembers (req, res) { * @param req the request * @param res the response */ -async function searchInvites (req, res) { - const result = await service.searchInvites(req.authUser, req.params.id, req.query) - res.send(result.result) +async function searchInvites(req, res) { + const result = await service.searchInvites( + req.authUser, + req.params.id, + req.query + ); + res.send(result.result); } /** @@ -78,9 +90,13 @@ async function searchInvites (req, res) { * @param req the request * @param res the response */ -async function deleteMember (req, res) { - await service.deleteMember(req.authUser, req.params.id, req.params.projectMemberId) - res.status(HttpStatus.NO_CONTENT).end() +async function deleteMember(req, res) { + await service.deleteMember( + req.authUser, + req.params.id, + req.params.projectMemberId + ); + res.status(HttpStatus.NO_CONTENT).end(); } /** @@ -88,8 +104,17 @@ async function deleteMember (req, res) { * @param req the request * @param res the response */ -async function getMe (req, res) { - res.send(await service.getMe(req.authUser)) +async function getMe(req, res) { + res.send(await service.getMe(req.authUser)); +} + +/** + * + * @param req the request + * @param res the response + */ +async function createProj(req, res) { + res.send(await service.createProj(req.authUser, req.body)); } module.exports = { @@ -101,5 +126,6 @@ module.exports = { searchMembers, searchInvites, deleteMember, - getMe -} + getMe, + createProj, +}; diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index f5d062c6..9bbe25c6 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -1,7 +1,7 @@ /** * Contains taas team routes */ -const constants = require('../../app-constants') +const constants = require('../../app-constants'); module.exports = { '/taas-teams': { @@ -9,77 +9,85 @@ module.exports = { controller: 'TeamController', method: 'searchTeams', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/email': { post: { controller: 'TeamController', method: 'sendEmail', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/skills': { get: { controller: 'SkillController', method: 'searchSkills', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/me': { get: { controller: 'TeamController', method: 'getMe', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/:id': { get: { controller: 'TeamController', method: 'getTeam', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/:id/jobs/:jobId': { get: { controller: 'TeamController', method: 'getTeamJob', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/:id/members': { post: { controller: 'TeamController', method: 'addMembers', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] + scopes: [constants.Scopes.READ_TAAS_TEAM], }, get: { controller: 'TeamController', method: 'searchMembers', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/:id/invites': { get: { controller: 'TeamController', method: 'searchInvites', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, }, '/taas-teams/:id/members/:projectMemberId': { delete: { controller: 'TeamController', method: 'deleteMember', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM] - } - } -} + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, + }, + '/taas-teams/createTeamRequest': { + post: { + controller: 'TeamController', + method: 'createProj', + auth: 'jwt', + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, + }, +}; diff --git a/src/services/TeamService.js b/src/services/TeamService.js index a1432fd1..3f6dbfd3 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -2,16 +2,16 @@ * This service provides operations of Job. */ -const _ = require('lodash') -const Joi = require('joi') -const dateFNS = require('date-fns') -const config = require('config') -const emailTemplateConfig = require('../../config/email_template.config') -const helper = require('../common/helper') -const logger = require('../common/logger') -const errors = require('../common/errors') -const JobService = require('./JobService') -const ResourceBookingService = require('./ResourceBookingService') +const _ = require('lodash'); +const Joi = require('joi'); +const dateFNS = require('date-fns'); +const config = require('config'); +const emailTemplateConfig = require('../../config/email_template.config'); +const helper = require('../common/helper'); +const logger = require('../common/logger'); +const errors = require('../common/errors'); +const JobService = require('./JobService'); +const ResourceBookingService = require('./ResourceBookingService'); const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { return { @@ -20,9 +20,9 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { from: template.from, recipients: template.recipients, cc: template.cc, - sendgridTemplateId: template.sendgridTemplateId - } -}) + sendgridTemplateId: template.sendgridTemplateId, + }; +}); /** * Function to get placed resource bookings with specific projectIds @@ -30,10 +30,14 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { * @param {Array} projectIds project ids * @returns the request result */ -async function _getPlacedResourceBookingsByProjectIds (currentUser, projectIds) { - const criteria = { status: 'placed', projectIds } - const { result } = await ResourceBookingService.searchResourceBookings(currentUser, criteria, { returnAll: true }) - return result +async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { + const criteria = { status: 'placed', projectIds }; + const { result } = await ResourceBookingService.searchResourceBookings( + currentUser, + criteria, + { returnAll: true } + ); + return result; } /** @@ -42,9 +46,13 @@ async function _getPlacedResourceBookingsByProjectIds (currentUser, projectIds) * @param {Array} projectIds project ids * @returns the request result */ -async function _getJobsByProjectIds (currentUser, projectIds) { - const { result } = await JobService.searchJobs(currentUser, { projectIds }, { returnAll: true }) - return result +async function _getJobsByProjectIds(currentUser, projectIds) { + const { result } = await JobService.searchJobs( + currentUser, + { projectIds }, + { returnAll: true } + ); + return result; } /** @@ -53,40 +61,59 @@ async function _getJobsByProjectIds (currentUser, projectIds) { * @param {Object} criteria the search criteria * @returns {Object} the search result, contain total/page/perPage and result array */ -async function searchTeams (currentUser, criteria) { - const sort = `${criteria.sortBy} ${criteria.sortOrder}` +async function searchTeams(currentUser, criteria) { + const sort = `${criteria.sortBy} ${criteria.sortOrder}`; // Get projects from /v5/projects with searching criteria - const { total, page, perPage, result: projects } = await helper.getProjects( - currentUser, - { - page: criteria.page, - perPage: criteria.perPage, - name: criteria.name, - sort - } - ) + const { + total, + page, + perPage, + result: projects, + } = await helper.getProjects(currentUser, { + page: criteria.page, + perPage: criteria.perPage, + name: criteria.name, + sort, + }); return { total, page, perPage, - result: await getTeamDetail(currentUser, projects) - } + result: await getTeamDetail(currentUser, projects), + }; } -searchTeams.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - criteria: Joi.object().keys({ - page: Joi.page(), - perPage: Joi.perPage(), - sortBy: Joi.string().valid('createdAt', 'updatedAt', 'lastActivityAt', 'id', 'status', 'name', 'type', 'best match').default('lastActivityAt'), - sortOrder: Joi.when('sortBy', { - is: 'best match', - then: Joi.forbidden().label('sortOrder(with sortBy being `best match`)'), - otherwise: Joi.string().valid('asc', 'desc').default('desc') - }), - name: Joi.string() - }).required() -}).required() +searchTeams.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + criteria: Joi.object() + .keys({ + page: Joi.page(), + perPage: Joi.perPage(), + sortBy: Joi.string() + .valid( + 'createdAt', + 'updatedAt', + 'lastActivityAt', + 'id', + 'status', + 'name', + 'type', + 'best match' + ) + .default('lastActivityAt'), + sortOrder: Joi.when('sortBy', { + is: 'best match', + then: Joi.forbidden().label( + 'sortOrder(with sortBy being `best match`)' + ), + otherwise: Joi.string().valid('asc', 'desc').default('desc'), + }), + name: Joi.string(), + }) + .required(), + }) + .required(); /** * Get team details @@ -95,120 +122,142 @@ searchTeams.schema = Joi.object().keys({ * @param {Object} isSearch the flag whether for search function * @returns {Object} the search result */ -async function getTeamDetail (currentUser, projects, isSearch = true) { - const projectIds = _.map(projects, 'id') +async function getTeamDetail(currentUser, projects, isSearch = true) { + const projectIds = _.map(projects, 'id'); // Get all placed resourceBookings filtered by projectIds - const resourceBookings = await _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) + const resourceBookings = await _getPlacedResourceBookingsByProjectIds( + currentUser, + projectIds + ); // Get all jobs filtered by projectIds - const jobs = await _getJobsByProjectIds(currentUser, projectIds) + const jobs = await _getJobsByProjectIds(currentUser, projectIds); // Get first week day and last week day - const curr = new Date() - const firstDay = dateFNS.startOfWeek(curr) - const lastDay = dateFNS.endOfWeek(curr) + const curr = new Date(); + const firstDay = dateFNS.startOfWeek(curr); + const lastDay = dateFNS.endOfWeek(curr); - logger.debug({ component: 'TeamService', context: 'getTeamDetail', message: `week started: ${firstDay}, week ended: ${lastDay}` }) + logger.debug({ + component: 'TeamService', + context: 'getTeamDetail', + message: `week started: ${firstDay}, week ended: ${lastDay}`, + }); - const result = [] + const result = []; for (const project of projects) { - const rbs = _.filter(resourceBookings, { projectId: project.id }) - const res = _.clone(project) - res.weeklyCost = 0 - res.resources = [] + const rbs = _.filter(resourceBookings, { projectId: project.id }); + const res = _.clone(project); + res.weeklyCost = 0; + res.resources = []; if (rbs && rbs.length > 0) { // Get minimal start date and maximal end date - const startDates = [] - const endDates = [] + const startDates = []; + const endDates = []; for (const rbsItem of rbs) { if (rbsItem.startDate) { - startDates.push(new Date(rbsItem.startDate)) + startDates.push(new Date(rbsItem.startDate)); } if (rbsItem.endDate) { - endDates.push(new Date(rbsItem.endDate)) + endDates.push(new Date(rbsItem.endDate)); } } if (startDates && startDates.length > 0) { - res.startDate = _.min(startDates) + res.startDate = _.min(startDates); } if (endDates && endDates.length > 0) { - res.endDate = _.max(endDates) + res.endDate = _.max(endDates); } // Count weekly rate for (const item of rbs) { // ignore any resourceBooking that has customerRate missed if (!item.customerRate) { - continue + continue; } - const startDate = new Date(item.startDate) - const endDate = new Date(item.endDate) + const startDate = new Date(item.startDate); + const endDate = new Date(item.endDate); // normally startDate is smaller than endDate for a resourceBooking so not check if startDate < endDate - if ((!item.startDate || startDate < lastDay) && - (!item.endDate || endDate > firstDay)) { - res.weeklyCost += item.customerRate + if ( + (!item.startDate || startDate < lastDay) && + (!item.endDate || endDate > firstDay) + ) { + res.weeklyCost += item.customerRate; } } const resourceInfos = await Promise.all( _.map(rbs, (rb) => { - return helper.getUserById(rb.userId, true) - .then(user => { - const resource = { - id: rb.id, - userId: user.id, - ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']) - } - // If call function is not search, add jobId field - if (!isSearch) { - resource.jobId = rb.jobId - resource.customerRate = rb.customerRate - resource.startDate = rb.startDate - resource.endDate = rb.endDate - } - return resource - }) - })) + return helper.getUserById(rb.userId, true).then((user) => { + const resource = { + id: rb.id, + userId: user.id, + ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']), + }; + // If call function is not search, add jobId field + if (!isSearch) { + resource.jobId = rb.jobId; + resource.customerRate = rb.customerRate; + resource.startDate = rb.startDate; + resource.endDate = rb.endDate; + } + return resource; + }); + }) + ); if (resourceInfos && resourceInfos.length > 0) { - res.resources = resourceInfos + res.resources = resourceInfos; - const userHandles = _.map(resourceInfos, 'handle') + const userHandles = _.map(resourceInfos, 'handle'); // Get user photo from /v5/members - const members = await helper.getMembers(userHandles) + const members = await helper.getMembers(userHandles); for (const item of res.resources) { - const findMember = _.find(members, { handleLower: item.handle.toLowerCase() }) + const findMember = _.find(members, { + handleLower: item.handle.toLowerCase(), + }); if (findMember && findMember.photoURL) { - item.photo_url = findMember.photoURL + item.photo_url = findMember.photoURL; } } } } - const jobsTmp = _.filter(jobs, { projectId: project.id }) + const jobsTmp = _.filter(jobs, { projectId: project.id }); if (jobsTmp && jobsTmp.length > 0) { if (isSearch) { // Count total positions - res.totalPositions = 0 + res.totalPositions = 0; for (const item of jobsTmp) { // only sum numPositions of jobs whose status is NOT cancelled or closed if (['cancelled', 'closed'].includes(item.status)) { - continue + continue; } - res.totalPositions += item.numPositions + res.totalPositions += item.numPositions; } } else { - res.jobs = _.map(jobsTmp, job => { - return _.pick(job, ['id', 'description', 'startDate', 'duration', 'numPositions', 'rateType', 'skills', 'customerRate', 'status', 'title']) - }) + res.jobs = _.map(jobsTmp, (job) => { + return _.pick(job, [ + 'id', + 'description', + 'startDate', + 'duration', + 'numPositions', + 'rateType', + 'skills', + 'customerRate', + 'status', + 'title', + ]); + }); } } - result.push(res) + result.push(res); } - return result + return result; } /** @@ -217,31 +266,35 @@ async function getTeamDetail (currentUser, projects, isSearch = true) { * @param {String} id the job id * @returns {Object} the team */ -async function getTeam (currentUser, id) { - const project = await helper.getProjectById(currentUser, id) - const result = await getTeamDetail(currentUser, [project], false) - const teamDetail = result[0] +async function getTeam(currentUser, id) { + const project = await helper.getProjectById(currentUser, id); + const result = await getTeamDetail(currentUser, [project], false); + const teamDetail = result[0]; // add job skills for result - let jobSkills = [] + let jobSkills = []; if (teamDetail && teamDetail.jobs) { for (const job of teamDetail.jobs) { if (job.skills) { - const usersPromises = [] - _.map(job.skills, (skillId) => { usersPromises.push(helper.getSkillById(skillId)) }) - jobSkills = await Promise.all(usersPromises) - job.skills = jobSkills + const usersPromises = []; + _.map(job.skills, (skillId) => { + usersPromises.push(helper.getSkillById(skillId)); + }); + jobSkills = await Promise.all(usersPromises); + job.skills = jobSkills; } } } - return teamDetail + return teamDetail; } -getTeam.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required() -}).required() +getTeam.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + }) + .required(); /** * Get team job with id @@ -250,23 +303,25 @@ getTeam.schema = Joi.object().keys({ * @param {String} jobId the job id * @returns the team job */ -async function getTeamJob (currentUser, id, jobId) { - const project = await helper.getProjectById(currentUser, id) - const jobs = await _getJobsByProjectIds(currentUser, [project.id]) - const job = _.find(jobs, { id: jobId }) +async function getTeamJob(currentUser, id, jobId) { + const project = await helper.getProjectById(currentUser, id); + const jobs = await _getJobsByProjectIds(currentUser, [project.id]); + const job = _.find(jobs, { id: jobId }); if (!job) { - throw new errors.NotFoundError(`id: ${jobId} "Job" with Team id ${id} doesn't exist`) + throw new errors.NotFoundError( + `id: ${jobId} "Job" with Team id ${id} doesn't exist` + ); } const result = { id: job.id, - title: job.title - } + title: job.title, + }; if (job.skills) { result.skills = await Promise.all( _.map(job.skills, (skillId) => helper.getSkillById(skillId)) - ) + ); } // If the job has candidates, the following data for each candidate would be populated: @@ -278,36 +333,49 @@ async function getTeamJob (currentUser, id, jobId) { if (job && job.candidates && job.candidates.length > 0) { // find user data for candidates const users = await Promise.all( - _.map(_.uniq(_.map(job.candidates, 'userId')), userId => helper.getUserById(userId, true)) - ) - const userMap = _.groupBy(users, 'id') + _.map(_.uniq(_.map(job.candidates, 'userId')), (userId) => + helper.getUserById(userId, true) + ) + ); + const userMap = _.groupBy(users, 'id'); // find photo URLs for users - const members = await helper.getMembers(_.map(users, 'handle')) - const photoURLMap = _.groupBy(members, 'handleLower') - - result.candidates = _.map(job.candidates, candidate => { - const candidateData = _.pick(candidate, ['status', 'resume', 'userId', 'interviews', 'id']) - const userData = userMap[candidate.userId][0] + const members = await helper.getMembers(_.map(users, 'handle')); + const photoURLMap = _.groupBy(members, 'handleLower'); + + result.candidates = _.map(job.candidates, (candidate) => { + const candidateData = _.pick(candidate, [ + 'status', + 'resume', + 'userId', + 'interviews', + 'id', + ]); + const userData = userMap[candidate.userId][0]; // attach user data to the candidate - Object.assign(candidateData, _.pick(userData, ['handle', 'firstName', 'lastName', 'skills'])) + Object.assign( + candidateData, + _.pick(userData, ['handle', 'firstName', 'lastName', 'skills']) + ); // attach photo URL to the candidate - const handleLower = userData.handle.toLowerCase() + const handleLower = userData.handle.toLowerCase(); if (photoURLMap[handleLower]) { - candidateData.photo_url = photoURLMap[handleLower][0].photoURL + candidateData.photo_url = photoURLMap[handleLower][0].photoURL; } - return candidateData - }) + return candidateData; + }); } - return result + return result; } -getTeamJob.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required(), - jobId: Joi.string().guid().required() -}).required() +getTeamJob.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + jobId: Joi.string().guid().required(), + }) + .required(); /** * Send email through a particular template @@ -315,18 +383,21 @@ getTeamJob.schema = Joi.object().keys({ * @param {Object} data the email object * @returns {undefined} */ -async function sendEmail (currentUser, data) { - const template = emailTemplates[data.template] - const dataCC = data.cc || [] - const templateCC = template.cc || [] - const dataRecipients = data.recipients || [] - const templateRecipients = template.recipients || [] +async function sendEmail(currentUser, data) { + const template = emailTemplates[data.template]; + const dataCC = data.cc || []; + const templateCC = template.cc || []; + const dataRecipients = data.recipients || []; + const templateRecipients = template.recipients || []; const subjectBody = { subject: data.subject || template.subject, - body: data.body || template.body - } + body: data.body || template.body, + }; for (const key in subjectBody) { - subjectBody[key] = await helper.substituteStringByObject(subjectBody[key], data.data) + subjectBody[key] = await helper.substituteStringByObject( + subjectBody[key], + data.data + ); } const emailData = { // override template if coming data already have the 'from' address @@ -336,21 +407,27 @@ async function sendEmail (currentUser, data) { cc: _.uniq([...dataCC, ...templateCC]), data: { ...data.data, ...subjectBody }, sendgrid_template_id: template.sendgridTemplateId, - version: 'v3' - } - await helper.postEvent(config.EMAIL_TOPIC, emailData) + version: 'v3', + }; + await helper.postEvent(config.EMAIL_TOPIC, emailData); } -sendEmail.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - data: Joi.object().keys({ - template: Joi.string().valid(...Object.keys(emailTemplates)).required(), - data: Joi.object().required(), - from: Joi.string().email(), - recipients: Joi.array().items(Joi.string().email()).allow(null), - cc: Joi.array().items(Joi.string().email()).allow(null) - }).required() -}).required() +sendEmail.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + data: Joi.object() + .keys({ + template: Joi.string() + .valid(...Object.keys(emailTemplates)) + .required(), + data: Joi.object().required(), + from: Joi.string().email(), + recipients: Joi.array().items(Joi.string().email()).allow(null), + cc: Joi.array().items(Joi.string().email()).allow(null), + }) + .required(), + }) + .required(); /** * Add a member to a team as customer. @@ -360,25 +437,25 @@ sendEmail.schema = Joi.object().keys({ * @param {String} fields the fields to be returned * @returns {Object} the member added */ -async function _addMemberToProjectAsCustomer (projectId, userId, fields) { +async function _addMemberToProjectAsCustomer(projectId, userId, fields) { try { const member = await helper.createProjectMember( projectId, { userId: userId, role: 'customer' }, { fields } - ) - return member + ); + return member; } catch (err) { - err.message = _.get(err, 'response.body.message') || err.message + err.message = _.get(err, 'response.body.message') || err.message; if (err.message && err.message.includes('User already registered')) { - throw new Error('User is already added') + throw new Error('User is already added'); } logger.error({ component: 'TeamService', context: '_addMemberToProjectAsCustomer', - message: err.message - }) - throw err + message: err.message, + }); + throw err; } } @@ -390,81 +467,112 @@ async function _addMemberToProjectAsCustomer (projectId, userId, fields) { * @param {Object} data the object including members with handle/email to be added * @returns {Object} the success/failed added members */ -async function addMembers (currentUser, id, criteria, data) { - await helper.getProjectById(currentUser, id) // check whether the user can access the project +async function addMembers(currentUser, id, criteria, data) { + await helper.getProjectById(currentUser, id); // check whether the user can access the project const result = { success: [], - failed: [] - } - - const handles = data.handles || [] - const emails = data.emails || [] - - const handleMembers = await helper.getMemberDetailsByHandles(handles) - .then((members) => _.map(members, (member) => ({ - ...member, - // populate members with lower-cased handle for case insensitive search - handleLowerCase: member.handle.toLowerCase() - }))) - - const emailMembers = await helper.getMemberDetailsByEmails(emails) - .then((members) => _.map(members, (member) => ({ - ...member, - // populate members with lower-cased email for case insensitive search - emailLowerCase: member.email.toLowerCase() - }))) + failed: [], + }; + + const handles = data.handles || []; + const emails = data.emails || []; + + const handleMembers = await helper + .getMemberDetailsByHandles(handles) + .then((members) => + _.map(members, (member) => ({ + ...member, + // populate members with lower-cased handle for case insensitive search + handleLowerCase: member.handle.toLowerCase(), + })) + ); + + const emailMembers = await helper + .getMemberDetailsByEmails(emails) + .then((members) => + _.map(members, (member) => ({ + ...member, + // populate members with lower-cased email for case insensitive search + emailLowerCase: member.email.toLowerCase(), + })) + ); await Promise.all([ - Promise.all(handles.map(handle => { - const memberDetails = _.find(handleMembers, { handleLowerCase: handle.toLowerCase() }) - - if (!memberDetails) { - result.failed.push({ error: 'User doesn\'t exist', handle }) - return - } - - return _addMemberToProjectAsCustomer(id, memberDetails.userId, criteria.fields) - .then(member => { - // note, that we return `handle` in the same case it was in request - result.success.push(({ ...member, handle })) - }).catch(err => { - result.failed.push({ error: err.message, handle }) - }) - })), - - Promise.all(emails.map(email => { - const memberDetails = _.find(emailMembers, { emailLowerCase: email.toLowerCase() }) - - if (!memberDetails) { - result.failed.push({ error: 'User doesn\'t exist', email }) - return - } + Promise.all( + handles.map((handle) => { + const memberDetails = _.find(handleMembers, { + handleLowerCase: handle.toLowerCase(), + }); + + if (!memberDetails) { + result.failed.push({ error: "User doesn't exist", handle }); + return; + } - return _addMemberToProjectAsCustomer(id, memberDetails.id, criteria.fields) - .then(member => { - // note, that we return `email` in the same case it was in request - result.success.push(({ ...member, email })) - }).catch(err => { - result.failed.push({ error: err.message, email }) - }) - })) - ]) + return _addMemberToProjectAsCustomer( + id, + memberDetails.userId, + criteria.fields + ) + .then((member) => { + // note, that we return `handle` in the same case it was in request + result.success.push({ ...member, handle }); + }) + .catch((err) => { + result.failed.push({ error: err.message, handle }); + }); + }) + ), + + Promise.all( + emails.map((email) => { + const memberDetails = _.find(emailMembers, { + emailLowerCase: email.toLowerCase(), + }); + + if (!memberDetails) { + result.failed.push({ error: "User doesn't exist", email }); + return; + } - return result + return _addMemberToProjectAsCustomer( + id, + memberDetails.id, + criteria.fields + ) + .then((member) => { + // note, that we return `email` in the same case it was in request + result.success.push({ ...member, email }); + }) + .catch((err) => { + result.failed.push({ error: err.message, email }); + }); + }) + ), + ]); + + return result; } -addMembers.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required(), - criteria: Joi.object().keys({ - fields: Joi.string() - }).required(), - data: Joi.object().keys({ - handles: Joi.array().items(Joi.string()), - emails: Joi.array().items(Joi.string().email()) - }).or('handles', 'emails').required() -}).required() +addMembers.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + criteria: Joi.object() + .keys({ + fields: Joi.string(), + }) + .required(), + data: Joi.object() + .keys({ + handles: Joi.array().items(Joi.string()), + emails: Joi.array().items(Joi.string().email()), + }) + .or('handles', 'emails') + .required(), + }) + .required(); /** * Search members in a team. @@ -475,19 +583,23 @@ addMembers.schema = Joi.object().keys({ * @params {Object} criteria the search criteria * @returns {Object} the search result */ -async function searchMembers (currentUser, id, criteria) { - const result = await helper.listProjectMembers(currentUser, id, criteria) - return { result } +async function searchMembers(currentUser, id, criteria) { + const result = await helper.listProjectMembers(currentUser, id, criteria); + return { result }; } -searchMembers.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required(), - criteria: Joi.object().keys({ - role: Joi.string(), - fields: Joi.string() - }).required() -}).required() +searchMembers.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + criteria: Joi.object() + .keys({ + role: Joi.string(), + fields: Joi.string(), + }) + .required(), + }) + .required(); /** * Search member invites for a team. @@ -498,18 +610,26 @@ searchMembers.schema = Joi.object().keys({ * @params {Object} criteria the search criteria * @returns {Object} the search result */ -async function searchInvites (currentUser, id, criteria) { - const result = await helper.listProjectMemberInvites(currentUser, id, criteria) - return { result } +async function searchInvites(currentUser, id, criteria) { + const result = await helper.listProjectMemberInvites( + currentUser, + id, + criteria + ); + return { result }; } -searchInvites.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required(), - criteria: Joi.object().keys({ - fields: Joi.string() - }).required() -}).required() +searchInvites.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + criteria: Joi.object() + .keys({ + fields: Joi.string(), + }) + .required(), + }) + .required(); /** * Remove a member from a team. @@ -520,15 +640,17 @@ searchInvites.schema = Joi.object().keys({ * @param {String} projectMemberId the id of the project member * @returns {undefined} */ -async function deleteMember (currentUser, id, projectMemberId) { - await helper.deleteProjectMember(currentUser, id, projectMemberId) +async function deleteMember(currentUser, id, projectMemberId) { + await helper.deleteProjectMember(currentUser, id, projectMemberId); } -deleteMember.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required(), - projectMemberId: Joi.number().integer().required() -}).required() +deleteMember.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + projectMemberId: Joi.number().integer().required(), + }) + .required(); /** * Return details about the current user. @@ -537,13 +659,31 @@ deleteMember.schema = Joi.object().keys({ * @params {Object} criteria the search criteria * @returns {Object} the user data for current user */ -async function getMe (currentUser) { - return helper.getUserByExternalId(currentUser.userId) +async function getMe(currentUser) { + return helper.getUserByExternalId(currentUser.userId); } -getMe.schema = Joi.object().keys({ - currentUser: Joi.object().required() -}).required() +getMe.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + }) + .required(); + +/** + * @param {Object} currentUser the user performing the operation. + * @param {Object} data project data + * @returns {Object} the created project + */ +async function createProj(currentUser, data) { + return helper.createProject(currentUser, data); +} + +createProj.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + data: Joi.object().required(), + }) + .required(); module.exports = { searchTeams, @@ -554,5 +694,6 @@ module.exports = { searchMembers, searchInvites, deleteMember, - getMe -} + getMe, + createProj, +}; From b16a6fc68707155c2c923336689ddcb7621dd96a Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Thu, 27 May 2021 12:14:58 +0300 Subject: [PATCH 15/86] chore: use v5/members instead of v3/members --- config/default.js | 2 +- src/common/helper.js | 32 +++++++++++++------------------- src/services/PaymentService.js | 2 +- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/config/default.js b/config/default.js index 2b5ca7ba..1a3090bf 100644 --- a/config/default.js +++ b/config/default.js @@ -40,7 +40,7 @@ module.exports = { TOPCODER_USERS_API: process.env.TOPCODER_USERS_API || 'https://api.topcoder-dev.com/v3/users', // the api to find topcoder members - TOPCODER_MEMBERS_API: process.env.TOPCODER_MEMBERS_API || 'https://api.topcoder-dev.com/v3/members', + TOPCODER_MEMBERS_API: process.env.TOPCODER_MEMBERS_API || 'https://api.topcoder-dev.com/v5/members', // rate limit of requests to user api MAX_PARALLEL_REQUEST_TOPCODER_USERS_API: process.env.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API || 100, diff --git a/src/common/helper.js b/src/common/helper.js index 0ce11905..e68e5c58 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1304,13 +1304,10 @@ async function getMemberDetailsByHandles(handles) { } const token = await getM2MToken(); const res = await request - .get(`${config.TOPCODER_MEMBERS_API}/_search`) + .get(`${config.TOPCODER_MEMBERS_API}/`) .query({ - query: _.map( - handles, - (handle) => `handleLower:${handle.toLowerCase()}` - ).join(' OR '), - fields: 'userId,handle,firstName,lastName,email', + 'handlesLower[]': handles.map(handle => handle.toLowerCase()), + fields: 'userId,handle,handleLower,firstName,lastName,email', }) .set('Authorization', `Bearer ${token}`) .set('Accept', 'application/json'); @@ -1318,7 +1315,7 @@ async function getMemberDetailsByHandles(handles) { context: 'getMemberDetailsByHandles', message: `response body: ${JSON.stringify(res.body)}`, }); - return _.get(res.body, 'result.content'); + return res.body; } /** @@ -1327,17 +1324,14 @@ async function getMemberDetailsByHandles(handles) { * @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'); +async function getMemberDetailsByHandle(handle) { + const [memberDetails] = await getMemberDetailsByHandles([handle]) + + if (!memberDetails) { + throw new errors.NotFoundError(`Member details are not found by handle "${handle}".`) + } + + return memberDetails } /** @@ -1756,7 +1750,7 @@ module.exports = { getAuditM2Muser, checkIsMemberOfProject, getMemberDetailsByHandles, - getV3MemberDetailsByHandle, + getMemberDetailsByHandle, getMemberDetailsByEmails, createProjectMember, listProjectMembers, diff --git a/src/services/PaymentService.js b/src/services/PaymentService.js index d06ad671..e61a6c34 100644 --- a/src/services/PaymentService.js +++ b/src/services/PaymentService.js @@ -153,7 +153,7 @@ async function activateChallenge (id, token) { async function closeChallenge (id, userHandle, token) { localLogger.info({ context: 'closeChallenge', message: `Closing challenge ${id}` }) try { - const { userId } = await helper.getV3MemberDetailsByHandle(userHandle) + const { userId } = await helper.getMemberDetailsByHandle(userHandle) const body = { status: constants.ChallengeStatus.COMPLETED, winners: [{ From 3ede43e70d7f0111950e3e3f5c8faf0e98f1b18b Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Thu, 27 May 2021 15:49:58 +0530 Subject: [PATCH 16/86] fix: addded new 'offred' status --- src/bootstrap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bootstrap.js b/src/bootstrap.js index 2999f131..8bfa6df2 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -16,7 +16,7 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') -Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed') +Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed','offered') Joi.title = () => Joi.string().max(128) Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) From 93495aa0871333e8bd8ed4d698d8b634770f7e1b Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Thu, 27 May 2021 18:36:42 +0530 Subject: [PATCH 17/86] Updated Swagger for jobCandidate statuses --- docs/swagger.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a0b6064b..3bd72c09 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -603,6 +603,13 @@ paths: "cancelled", "interview", "topcoder-rejected", + "applied", + "rejected-pre-screen", + "skills-test", + "skills-test", + "phone-screen", + "job-closed", + "offered" ] description: The job candidate status. - in: query From 8446c87052640e2db0f7c57bf80aab131a393639 Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Thu, 27 May 2021 19:07:47 +0530 Subject: [PATCH 18/86] Update swagger.yaml --- docs/swagger.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 3bd72c09..d209a3fb 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -606,7 +606,6 @@ paths: "applied", "rejected-pre-screen", "skills-test", - "skills-test", "phone-screen", "job-closed", "offered" From 01faad42292d91cd36db27b2e07f306eea6084a3 Mon Sep 17 00:00:00 2001 From: dengjun Date: Sat, 29 May 2021 12:11:35 +0800 Subject: [PATCH 19/86] api-updates: challnege:30186701 --- data/demo-data.json | 69 +- ...coder-bookings-api.postman_collection.json | 157 ++- docs/swagger.yaml | 106 +- ...-28-add-fields-to-job-and-job-candidate.js | 55 + src/bootstrap.js | 2 +- src/common/helper.js | 1083 +++++++++-------- src/controllers/TeamController.js | 62 +- src/models/Job.js | 30 + src/models/JobCandidate.js | 3 + src/routes/TeamRoutes.js | 48 +- src/services/InterviewService.js | 4 +- src/services/JobCandidateService.js | 9 +- src/services/JobService.js | 24 +- src/services/ResourceBookingService.js | 1 + src/services/TeamService.js | 390 +++--- 15 files changed, 1159 insertions(+), 884 deletions(-) create mode 100644 migrations/2021-05-28-add-fields-to-job-and-job-candidate.js diff --git a/data/demo-data.json b/data/demo-data.json index e0733443..30f547ae 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -20,6 +20,12 @@ ], "status": "in-review", "isApplicationPageActive": false, + "minSalary": 100, + "maxSalary": 200, + "hoursPerWeek": 20, + "jobLocation": "Any location", + "jobTimezone": "GMT", + "currency": "USD", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:21:10.394Z", @@ -45,6 +51,12 @@ ], "status": "in-review", "isApplicationPageActive": false, + "minSalary": 100, + "maxSalary": 200, + "hoursPerWeek": 20, + "jobLocation": "Any location", + "jobTimezone": "GMT", + "currency": "USD", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:11:26.934Z", @@ -70,6 +82,12 @@ ], "status": "in-review", "isApplicationPageActive": false, + "minSalary": 100, + "maxSalary": 200, + "hoursPerWeek": 20, + "jobLocation": "Any location", + "jobTimezone": "GMT", + "currency": "USD", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:18.595Z", @@ -95,6 +113,12 @@ ], "status": "in-review", "isApplicationPageActive": false, + "minSalary": 100, + "maxSalary": 200, + "hoursPerWeek": 20, + "jobLocation": "Any location", + "jobTimezone": "GMT", + "currency": "USD", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:12:09.293Z", @@ -109,6 +133,7 @@ "status": "open", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:14:05.412Z", @@ -122,6 +147,7 @@ "status": "open", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:14:41.500Z", @@ -135,6 +161,7 @@ "status": "open", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:14:43.985Z", @@ -148,6 +175,7 @@ "status": "open", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:14:46.310Z", @@ -161,6 +189,7 @@ "status": "open", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:14:48.449Z", @@ -174,6 +203,7 @@ "status": "open", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:14:50.595Z", @@ -184,7 +214,7 @@ "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", "googleCalendarId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, "attendeesList": null, @@ -203,6 +233,7 @@ "status": "open", "externalId": "88774631", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:16:34.914Z", @@ -213,7 +244,7 @@ "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", "googleCalendarId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, "attendeesList": null, @@ -228,7 +259,7 @@ "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", "googleCalendarId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, "attendeesList": null, @@ -247,6 +278,7 @@ "status": "open", "externalId": "88774631", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:21:13.939Z", @@ -257,7 +289,7 @@ "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", "googleCalendarId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 3, "startTimestamp": null, "attendeesList": [ @@ -275,7 +307,7 @@ "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", "googleCalendarId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, "attendeesList": [ @@ -293,7 +325,7 @@ "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", "googleCalendarId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, "attendeesList": null, @@ -312,6 +344,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:23.420Z", @@ -325,6 +358,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:41.691Z", @@ -338,6 +372,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:35.819Z", @@ -351,6 +386,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:39.914Z", @@ -364,6 +400,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:37.962Z", @@ -377,6 +414,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:43.611Z", @@ -390,6 +428,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:47.468Z", @@ -403,6 +442,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:45.506Z", @@ -416,6 +456,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:57.355Z", @@ -429,6 +470,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:49.415Z", @@ -442,6 +484,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:51.500Z", @@ -455,6 +498,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:53.297Z", @@ -468,6 +512,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:55.300Z", @@ -481,6 +526,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:59.282Z", @@ -494,6 +540,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:24:01.429Z", @@ -507,6 +554,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:24:03.511Z", @@ -520,6 +568,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:24:05.605Z", @@ -533,6 +582,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:15:05.339Z", @@ -546,6 +596,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:15:03.606Z", @@ -559,6 +610,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:14:59.106Z", @@ -572,6 +624,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:15:02.183Z", @@ -585,6 +638,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:15:00.676Z", @@ -598,6 +652,7 @@ "status": "placed", "externalId": "300234321", "resume": "http://example.com", + "remark": "excellent", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:15:06.739Z", @@ -2053,4 +2108,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 74155753..4070d520 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", + "_postman_id": "ca01f845-9ba8-473d-b005-fc200ac3cd39", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -33,7 +33,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -77,7 +77,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -103,8 +103,7 @@ "listen": "test", "script": { "exec": [ - "var data = JSON.parse(responseBody);\r", - "postman.setEnvironmentVariable(\"jobId\",data.id);" + "var data = JSON.parse(responseBody);" ], "type": "text/javascript" } @@ -121,7 +120,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -165,7 +164,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -208,7 +207,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -251,7 +250,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -991,7 +990,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1024,7 +1023,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1057,7 +1056,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1103,7 +1102,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description updated\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description updated\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1136,7 +1135,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description updated\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description updated\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1169,7 +1168,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1202,7 +1201,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1235,7 +1234,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1268,7 +1267,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1301,7 +1300,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1347,7 +1346,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description updated 2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description updated 2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1380,7 +1379,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description updated 2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description updated 2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1413,7 +1412,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1446,7 +1445,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -1707,7 +1706,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -1751,7 +1750,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobIdCreatedByM2M}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobIdCreatedByM2M}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -1795,7 +1794,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -1839,7 +1838,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -1883,7 +1882,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -1926,7 +1925,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2672,7 +2671,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2705,7 +2704,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobIdCreatedByM2M}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobIdCreatedByM2M}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2738,7 +2737,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2771,7 +2770,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2804,7 +2803,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2837,7 +2836,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2870,7 +2869,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2903,7 +2902,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2936,7 +2935,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -2969,7 +2968,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -3002,7 +3001,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -3035,7 +3034,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"status\": \"selected\",\n \"externalId\": \"300234321\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -3225,7 +3224,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -3274,7 +3273,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -3323,7 +3322,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -3372,7 +3371,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -3421,7 +3420,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -3470,7 +3469,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\"\n}", + "raw": "{\n \"jobId\": \"{{jobId}}\",\n \"userId\": \"95e7970f-12b4-43b7-ab35-38c34bf033c7\",\n \"externalId\": \"88774631\",\n \"resume\": \"http://example.com\",\n \"remark\": \"excellent\"\n}", "options": { "raw": { "language": "json" @@ -7946,7 +7945,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -7995,7 +7994,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -11436,7 +11435,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -14739,7 +14738,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -18052,7 +18051,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job1\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"ee4c50c1-c8c3-475e-b6b6-edbd136a19d6\",\n \"89139c80-d0a2-47c2-aa16-14589d5afd10\",\n \"9f2d9127-6a2e-4506-ad76-c4ab63577b09\",\n \"9515e7ee-83b6-49d1-ba5c-6c59c5a8ef1b\",\n \"c854ab55-5922-4be1-8ecc-b3bc1f8629af\",\n \"8456002e-fa2d-44f0-b0e7-86b1c02b6e4c\",\n \"114b4ec8-805e-4c60-b351-14a955a991a9\",\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\",\n \"23839f38-6f19-4de9-9d28-f020056bca73\",\n \"289e42a3-23e9-49be-88e1-6deb93cd8c31\",\n \"b403f209-63b5-42bc-9b5f-1564416640d8\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job1\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"ee4c50c1-c8c3-475e-b6b6-edbd136a19d6\",\n \"89139c80-d0a2-47c2-aa16-14589d5afd10\",\n \"9f2d9127-6a2e-4506-ad76-c4ab63577b09\",\n \"9515e7ee-83b6-49d1-ba5c-6c59c5a8ef1b\",\n \"c854ab55-5922-4be1-8ecc-b3bc1f8629af\",\n \"8456002e-fa2d-44f0-b0e7-86b1c02b6e4c\",\n \"114b4ec8-805e-4c60-b351-14a955a991a9\",\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\",\n \"23839f38-6f19-4de9-9d28-f020056bca73\",\n \"289e42a3-23e9-49be-88e1-6deb93cd8c31\",\n \"b403f209-63b5-42bc-9b5f-1564416640d8\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -18106,7 +18105,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\",\n \"23839f38-6f19-4de9-9d28-f020056bca73\",\n \"289e42a3-23e9-49be-88e1-6deb93cd8c31\",\n \"b403f209-63b5-42bc-9b5f-1564416640d8\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\",\n \"23839f38-6f19-4de9-9d28-f020056bca73\",\n \"289e42a3-23e9-49be-88e1-6deb93cd8c31\",\n \"b403f209-63b5-42bc-9b5f-1564416640d8\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -18756,7 +18755,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job3\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job3\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -20393,7 +20392,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job4\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"8456002e-fa2d-44f0-b0e7-86b1c02b6e4c\",\n \"114b4ec8-805e-4c60-b351-14a955a991a9\",\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job4\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"8456002e-fa2d-44f0-b0e7-86b1c02b6e4c\",\n \"114b4ec8-805e-4c60-b351-14a955a991a9\",\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -20493,7 +20492,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job5\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job5\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -20596,7 +20595,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -20747,7 +20746,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -20780,7 +20779,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -20979,7 +20978,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"jobId\": \"{{job_id_created_by_administrator}}\",\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"status\": \"placed\"\r\n}", + "raw": "{\r\n \"jobId\": \"{{job_id_created_by_administrator}}\",\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"status\": \"placed\",\r\n \"remark\": \"excellent\"\r\n}", "options": { "raw": { "language": "json" @@ -21098,7 +21097,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -21461,7 +21460,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -21757,7 +21756,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -22156,7 +22155,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_17234}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_17234}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -22577,7 +22576,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -22727,7 +22726,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -22760,7 +22759,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -23007,7 +23006,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"jobId\": \"{{job_id_created_by_member}}\",\r\n \"userId\": \"fe38eed1-af73-41fd-85a2-ac4da1ff09a3\",\r\n \"status\": \"placed\"\r\n}", + "raw": "{\r\n \"jobId\": \"{{job_id_created_by_member}}\",\r\n \"userId\": \"fe38eed1-af73-41fd-85a2-ac4da1ff09a3\",\r\n \"status\": \"placed\",\r\n \"remark\": \"excellent\"\r\n}", "options": { "raw": { "language": "json" @@ -23126,7 +23125,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -23541,7 +23540,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -23885,7 +23884,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -24342,7 +24341,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_17234}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_17234}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -24818,7 +24817,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -25250,7 +25249,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"jobId\": \"{{job_id_created_by_connect_manager}}\",\r\n \"userId\": \"fe38eed1-af73-41fd-85a2-ac4da1ff09a3\",\r\n \"status\": \"placed\"\r\n}", + "raw": "{\r\n \"jobId\": \"{{job_id_created_by_connect_manager}}\",\r\n \"userId\": \"fe38eed1-af73-41fd-85a2-ac4da1ff09a3\",\r\n \"status\": \"placed\",\r\n \"remark\": \"excellent\"\r\n}", "options": { "raw": { "language": "json" @@ -25369,7 +25368,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"88774632\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -25784,7 +25783,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -26124,7 +26123,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -26575,7 +26574,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_17234}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_17234}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", "options": { "raw": { "language": "json" @@ -27007,4 +27006,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1584f5cf..4cbce8f0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -613,7 +613,7 @@ paths: "skills-test", "phone-screen", "job-closed", - "offered" + "offered", ] description: The job candidate status. - in: query @@ -3290,6 +3290,12 @@ components: - numPositions - skills - status + - minSalary + - maxSalary + - hoursPerWeek + - jobLocation + - jobTimezone + - currency - createdAt - createdBy properties: @@ -3358,6 +3364,30 @@ components: isApplicationPageActive: type: boolean default: false + minSalary: + type: integer + example: 1000 + description: "the amount of minimum salary" + maxSalary: + type: integer + example: 3000 + description: "the amount of maximum salary" + hoursPerWeek: + type: integer + example: 20 + description: "the amount working hours per week" + jobLocation: + type: string + example: "Any location" + description: "the location of job" + jobTimezone: + type: string + example: "GMT" + description: "the timezone of job" + currency: + type: string + example: "USD" + description: "the currency of job" createdAt: type: string format: date-time @@ -3389,6 +3419,12 @@ components: - title - numPositions - skills + - minSalary + - maxSalary + - hoursPerWeek + - jobLocation + - jobTimezone + - currency properties: projectId: type: integer @@ -3447,6 +3483,30 @@ components: isApplicationPageActive: type: boolean default: false + minSalary: + type: integer + example: 1000 + description: "the amount of minimum salary" + maxSalary: + type: integer + example: 3000 + description: "the amount of maximum salary" + hoursPerWeek: + type: integer + example: 20 + description: "the amount working hours per week" + jobLocation: + type: string + example: "Any location" + description: "the location of job" + jobTimezone: + type: string + example: "GMT" + description: "the timezone of job" + currency: + type: string + example: "USD" + description: "the currency of job" JobCandidate: required: - id @@ -3490,6 +3550,10 @@ components: type: string example: "http://example.com" description: "The resume link" + remark: + type: string + example: "excellent" + description: "The remark of candidate" createdAt: type: string format: date-time @@ -3546,6 +3610,10 @@ components: type: string example: "http://example.com" description: "The resume link" + remark: + type: string + example: "excellent" + description: "The remark of candidate" interviews: type: array description: "Interviews associated to this job candidate." @@ -3605,6 +3673,10 @@ components: type: string example: "http://example.com" description: "The resume link" + remark: + type: string + example: "excellent" + description: "The remark of candidate" JobCandidatePatchRequestBody: properties: status: @@ -3629,6 +3701,10 @@ components: type: string example: "http://example.com" description: "The resume link" + remark: + type: string + example: "excellent" + description: "The remark of candidate" Interview: required: - id @@ -3888,6 +3964,30 @@ components: isApplicationPageActive: type: boolean default: false + minSalary: + type: integer + example: 1000 + description: "the amount of minimum salary" + maxSalary: + type: integer + example: 3000 + description: "the amount of maximum salary" + hoursPerWeek: + type: integer + example: 20 + description: "the amount working hours per week" + jobLocation: + type: string + example: "Any location" + description: "the location of job" + jobTimezone: + type: string + example: "GMT" + description: "the timezone of job" + currency: + type: string + example: "USD" + description: "the currency of job" ResourceBooking: required: - id @@ -4643,6 +4743,10 @@ components: type: string format: url description: "The link for the resume that can be downloaded" + remark: + type: string + example: "excellent" + description: "The remark of candidate" interviews: type: array items: diff --git a/migrations/2021-05-28-add-fields-to-job-and-job-candidate.js b/migrations/2021-05-28-add-fields-to-job-and-job-candidate.js new file mode 100644 index 00000000..472a25a8 --- /dev/null +++ b/migrations/2021-05-28-add-fields-to-job-and-job-candidate.js @@ -0,0 +1,55 @@ +const config = require('config') + +/* + * Add min_salary, max_salary, hours_per_week, job_location, job_timezone and currency fields to the Job model. + * Add remark field to the JobCandidate model. + */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'min_salary', + { type: Sequelize.INTEGER, allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'max_salary', + { type: Sequelize.INTEGER, allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'hours_per_week', + { type: Sequelize.INTEGER, allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'job_location', + { type: Sequelize.STRING(255), allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'job_timezone', + { type: Sequelize.STRING(128), allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'currency', + { type: Sequelize.STRING(30), allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'job_candidates', schema: config.DB_SCHEMA_NAME }, 'remark', + { type: Sequelize.STRING(255) }, + { transaction }) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + }, + down: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'min_salary', { transaction }) + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'max_salary', { transaction }) + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'hours_per_week', { transaction }) + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'job_location', { transaction }) + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'job_timezone', { transaction }) + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'currency', { transaction }) + await queryInterface.removeColumn({ tableName: 'job_candidates', schema: config.DB_SCHEMA_NAME }, 'remark', { transaction }) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + } +} diff --git a/src/bootstrap.js b/src/bootstrap.js index 8bfa6df2..2a1571c2 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -16,7 +16,7 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') -Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed','offered') +Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed', 'offered') Joi.title = () => Joi.string().max(128) Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) diff --git a/src/common/helper.js b/src/common/helper.js index e68e5c58..f3b950b0 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -2,50 +2,50 @@ * This file defines helper methods */ -const fs = require('fs'); -const querystring = require('querystring'); -const Confirm = require('prompt-confirm'); -const Bottleneck = require('bottleneck'); -const AWS = require('aws-sdk'); -const config = require('config'); -const HttpStatus = require('http-status-codes'); -const _ = require('lodash'); -const request = require('superagent'); -const elasticsearch = require('@elastic/elasticsearch'); +const fs = require('fs') +const querystring = require('querystring') +const Confirm = require('prompt-confirm') +const Bottleneck = require('bottleneck') +const AWS = require('aws-sdk') +const config = require('config') +const HttpStatus = require('http-status-codes') +const _ = require('lodash') +const request = require('superagent') +const elasticsearch = require('@elastic/elasticsearch') const { - ResponseError: ESResponseError, -} = require('@elastic/elasticsearch/lib/errors'); -const errors = require('../common/errors'); -const logger = require('./logger'); -const models = require('../models'); -const eventDispatcher = require('./eventDispatcher'); -const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper'); -const moment = require('moment'); + ResponseError: ESResponseError +} = require('@elastic/elasticsearch/lib/errors') +const errors = require('../common/errors') +const logger = require('./logger') +const models = require('../models') +const eventDispatcher = require('./eventDispatcher') +const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') +const moment = require('moment') const localLogger = { debug: (message) => logger.debug({ component: 'helper', context: message.context, - message: message.message, + message: message.message }), error: (message) => logger.error({ component: 'helper', context: message.context, - message: message.message, + message: message.message }), info: (message) => logger.info({ component: 'helper', context: message.context, - message: message.message, - }), -}; + message: message.message + }) +} -AWS.config.region = config.esConfig.AWS_REGION; +AWS.config.region = config.esConfig.AWS_REGION -const m2mAuth = require('tc-core-library-js').auth.m2m; +const m2mAuth = require('tc-core-library-js').auth.m2m const m2m = m2mAuth( _.pick(config, [ @@ -53,9 +53,9 @@ const m2m = m2mAuth( 'AUTH0_AUDIENCE', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', - 'AUTH0_PROXY_SERVER_URL', + 'AUTH0_PROXY_SERVER_URL' ]) -); +) const m2mForUbahn = m2mAuth({ AUTH0_AUDIENCE: config.AUTH0_AUDIENCE_UBAHN, @@ -64,20 +64,20 @@ const m2mForUbahn = m2mAuth({ 'TOKEN_CACHE_TIME', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', - 'AUTH0_PROXY_SERVER_URL', - ]), -}); + 'AUTH0_PROXY_SERVER_URL' + ]) +}) -let busApiClient; +let busApiClient /** * Get bus api client. * * @returns {Object} the bus api client */ -function getBusApiClient() { +function getBusApiClient () { if (busApiClient) { - return busApiClient; + return busApiClient } busApiClient = busApi( _.pick(config, [ @@ -88,17 +88,17 @@ function getBusApiClient() { 'AUTH0_CLIENT_SECRET', 'BUSAPI_URL', 'KAFKA_ERROR_TOPIC', - 'AUTH0_PROXY_SERVER_URL', + 'AUTH0_PROXY_SERVER_URL' ]) - ); - return busApiClient; + ) + return busApiClient } // ES Client mapping -const esClients = {}; +const esClients = {} // The es index property mapping -const esIndexPropertyMapping = {}; +const esIndexPropertyMapping = {} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { projectId: { type: 'integer' }, externalId: { type: 'keyword' }, @@ -113,17 +113,24 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { skills: { type: 'keyword' }, status: { type: 'keyword' }, isApplicationPageActive: { type: 'boolean' }, + minSalary: { type: 'integer' }, + maxSalary: { type: 'integer' }, + hoursPerWeek: { type: 'integer' }, + jobLocation: { type: 'keyword' }, + jobTimezone: { type: 'keyword' }, + currency: { type: 'keyword' }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { jobId: { type: 'keyword' }, userId: { type: 'keyword' }, status: { type: 'keyword' }, externalId: { type: 'keyword' }, resume: { type: 'text' }, + remark: { type: 'keyword' }, interviews: { type: 'nested', properties: { @@ -150,14 +157,14 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, updatedBy: { type: 'keyword' }, - deletedAt: { type: 'date' }, - }, + deletedAt: { type: 'date' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { projectId: { type: 'integer' }, userId: { type: 'keyword' }, @@ -195,32 +202,32 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, - }, + updatedBy: { type: 'keyword' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, - }, + updatedBy: { type: 'keyword' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} /** * Get the first parameter from cli arguments */ -function getParamFromCliArgs() { - const filteredArgs = process.argv.filter((arg) => !arg.includes('--')); +function getParamFromCliArgs () { + const filteredArgs = process.argv.filter((arg) => !arg.includes('--')) if (filteredArgs.length > 2) { - return filteredArgs[2]; + return filteredArgs[2] } - return null; + return null } /** @@ -228,18 +235,18 @@ function getParamFromCliArgs() { * @param {string} promptQuery the query to ask the user * @param {function} cb the callback function */ -async function promptUser(promptQuery, cb) { +async function promptUser (promptQuery, cb) { if (process.argv.includes('--force')) { - await cb(); - return; + await cb() + return } - const prompt = new Confirm(promptQuery); + const prompt = new Confirm(promptQuery) prompt.ask(async (answer) => { if (answer) { - await cb(); + await cb() } - }); + }) } /** @@ -248,23 +255,23 @@ async function promptUser(promptQuery, cb) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function createIndex(index, logger, esClient = null) { +async function createIndex (index, logger, esClient = null) { if (!esClient) { - esClient = getESClient(); + esClient = getESClient() } await esClient.indices.create({ index, body: { mappings: { - properties: esIndexPropertyMapping[index], - }, - }, - }); + properties: esIndexPropertyMapping[index] + } + } + }) logger.info({ component: 'createIndex', - message: `ES Index ${index} creation succeeded!`, - }); + message: `ES Index ${index} creation succeeded!` + }) } /** @@ -273,45 +280,45 @@ async function createIndex(index, logger, esClient = null) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function deleteIndex(index, logger, esClient = null) { +async function deleteIndex (index, logger, esClient = null) { if (!esClient) { - esClient = getESClient(); + esClient = getESClient() } - await esClient.indices.delete({ index }); + await esClient.indices.delete({ index }) logger.info({ component: 'deleteIndex', - message: `ES Index ${index} deletion succeeded!`, - }); + message: `ES Index ${index} deletion succeeded!` + }) } /** * Split data into bulks * @param {Array} data the array of data to split */ -function getBulksFromDocuments(data) { - const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6; - const bulks = []; - let documentIndex = 0; - let currentBulkSize = 0; - let currentBulk = []; +function getBulksFromDocuments (data) { + const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6 + const bulks = [] + let documentIndex = 0 + let currentBulkSize = 0 + let currentBulk = [] while (true) { // break loop when parsed all documents if (documentIndex >= data.length) { - bulks.push(currentBulk); - break; + bulks.push(currentBulk) + break } // check if current document size is greater than the max bulk size, if so, throw error const currentDocumentSize = Buffer.byteLength( JSON.stringify(data[documentIndex]), 'utf-8' - ); + ) if (maxBytes < currentDocumentSize) { throw new Error( `Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.` - ); + ) } if ( @@ -320,17 +327,17 @@ function getBulksFromDocuments(data) { ) { // if adding the current document goes over the max bulk size OR goes over max number of docs // then push the current bulk to bulks array and reset the current bulk - bulks.push(currentBulk); - currentBulk = []; - currentBulkSize = 0; + bulks.push(currentBulk) + currentBulk = [] + currentBulkSize = 0 } else { // otherwise, add document to current bulk - currentBulk.push(data[documentIndex]); - currentBulkSize += currentDocumentSize; - documentIndex++; + currentBulk.push(data[documentIndex]) + currentBulkSize += currentDocumentSize + documentIndex++ } } - return bulks; + return bulks } /** @@ -339,57 +346,57 @@ function getBulksFromDocuments(data) { * @param {Object} indexName the index name * @param {Object} logger the logger object */ -async function indexBulkDataToES(modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); +async function indexBulkDataToES (modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) logger.info({ component: 'indexBulkDataToES', - message: `Reindexing of ${modelName}s started!`, - }); + message: `Reindexing of ${modelName}s started!` + }) - const esClient = getESClient(); + const esClient = getESClient() // clear index - const indexExistsRes = await esClient.indices.exists({ index: indexName }); + const indexExistsRes = await esClient.indices.exists({ index: indexName }) if (indexExistsRes.statusCode !== 404) { - await deleteIndex(indexName, logger, esClient); + await deleteIndex(indexName, logger, esClient) } - await createIndex(indexName, logger, esClient); + await createIndex(indexName, logger, esClient) // get data from db logger.info({ component: 'indexBulkDataToES', - message: 'Getting data from database', - }); - const model = models[modelName]; - const data = await model.findAll({ include }); - const rawObjects = _.map(data, (r) => r.toJSON()); + message: 'Getting data from database' + }) + const model = models[modelName] + const data = await model.findAll({ include }) + const rawObjects = _.map(data, (r) => r.toJSON()) if (_.isEmpty(rawObjects)) { logger.info({ component: 'indexBulkDataToES', - message: `No data in database for ${modelName}`, - }); - return; + message: `No data in database for ${modelName}` + }) + return } - const bulks = getBulksFromDocuments(rawObjects); + const bulks = getBulksFromDocuments(rawObjects) - const startTime = Date.now(); - let doneCount = 0; + const startTime = Date.now() + let doneCount = 0 for (const bulk of bulks) { // send bulk to esclient const body = bulk.flatMap((doc) => [ { index: { _index: indexName, _id: doc.id } }, - doc, - ]); - await esClient.bulk({ refresh: true, body }); - doneCount += bulk.length; + doc + ]) + await esClient.bulk({ refresh: true, body }) + doneCount += bulk.length // log metrics - const timeSpent = Date.now() - startTime; - const avgTimePerDocument = timeSpent / doneCount; - const estimatedLength = avgTimePerDocument * data.length; - const timeLeft = startTime + estimatedLength - Date.now(); + const timeSpent = Date.now() - startTime + const avgTimePerDocument = timeSpent / doneCount + const estimatedLength = avgTimePerDocument * data.length + const timeLeft = startTime + estimatedLength - Date.now() logger.info({ component: 'indexBulkDataToES', message: `Processed ${doneCount} of ${ @@ -398,8 +405,8 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { avgTimePerDocument )}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime( timeLeft - )}`, - }); + )}` + }) } } @@ -410,36 +417,36 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { * @param {string} id the job id * @param {Object} logger the logger object */ -async function indexDataToEsById(id, modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); +async function indexDataToEsById (id, modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) logger.info({ component: 'indexDataToEsById', - message: `Reindexing of ${modelName} with id ${id} started!`, - }); - const esClient = getESClient(); + message: `Reindexing of ${modelName} with id ${id} started!` + }) + const esClient = getESClient() logger.info({ component: 'indexDataToEsById', - message: 'Getting data from database', - }); - const model = models[modelName]; + message: 'Getting data from database' + }) + const model = models[modelName] - const data = await model.findById(id, include); + const data = await model.findById(id, include) logger.info({ component: 'indexDataToEsById', - message: 'Indexing data into Elasticsearch', - }); + message: 'Indexing data into Elasticsearch' + }) await esClient.index({ index: indexName, id: id, - body: data.dataValues, - }); + body: data.dataValues + }) logger.info({ component: 'indexDataToEsById', - message: 'Indexing complete!', - }); + message: 'Indexing complete!' + }) } /** @@ -448,68 +455,68 @@ async function indexDataToEsById(id, modelOpts, indexName, logger) { * @param {Array} dataModels the data models to import * @param {Object} logger the logger object */ -async function importData(pathToFile, dataModels, logger) { +async function importData (pathToFile, dataModels, logger) { // check if file exists if (!fs.existsSync(pathToFile)) { - throw new Error(`File with path ${pathToFile} does not exist`); + throw new Error(`File with path ${pathToFile} does not exist`) } // clear database - logger.info({ component: 'importData', message: 'Clearing database...' }); - await models.sequelize.sync({ force: true }); + logger.info({ component: 'importData', message: 'Clearing database...' }) + await models.sequelize.sync({ force: true }) - let transaction = null; - let currentModelName = null; + let transaction = null + let currentModelName = null try { // Start a transaction - transaction = await models.sequelize.transaction(); - const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()); + transaction = await models.sequelize.transaction() + const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()) for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index]; - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); + const modelOpts = dataModels[index] + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) - currentModelName = modelName; - const model = models[modelName]; - const modelRecords = jsonData[modelName]; + currentModelName = modelName + const model = models[modelName] + const modelRecords = jsonData[modelName] if (modelRecords && modelRecords.length > 0) { logger.info({ component: 'importData', - message: `Importing data for model: ${modelName}`, - }); + message: `Importing data for model: ${modelName}` + }) - await model.bulkCreate(modelRecords, { include, transaction }); + await model.bulkCreate(modelRecords, { include, transaction }) logger.info({ component: 'importData', - message: `Records imported for model: ${modelName} = ${modelRecords.length}`, - }); + message: `Records imported for model: ${modelName} = ${modelRecords.length}` + }) } else { logger.info({ component: 'importData', - message: `No records to import for model: ${modelName}`, - }); + message: `No records to import for model: ${modelName}` + }) } } // commit transaction only if all things went ok logger.info({ component: 'importData', - message: 'committing transaction to database...', - }); - await transaction.commit(); + message: 'committing transaction to database...' + }) + await transaction.commit() } catch (error) { logger.error({ component: 'importData', - message: `Error while writing data of model: ${currentModelName}`, - }); + message: `Error while writing data of model: ${currentModelName}` + }) // rollback all insert operations if (transaction) { logger.info({ component: 'importData', - message: 'rollback database transaction...', - }); - transaction.rollback(); + message: 'rollback database transaction...' + }) + transaction.rollback() } if (error.name && error.errors && error.fields) { // For sequelize validation errors, we throw only fields with data that helps in debugging error, @@ -519,11 +526,11 @@ async function importData(pathToFile, dataModels, logger) { modelName: currentModelName, name: error.name, errors: error.errors, - fields: error.fields, + fields: error.fields }) - ); + ) } else { - throw error; + throw error } } @@ -533,10 +540,10 @@ async function importData(pathToFile, dataModels, logger) { include: [ { model: models.Interview, - as: 'interviews', - }, - ], - }; + as: 'interviews' + } + ] + } const resourceBookingModelOpts = { modelName: 'ResourceBooking', include: [ @@ -546,23 +553,23 @@ async function importData(pathToFile, dataModels, logger) { include: [ { model: models.WorkPeriodPayment, - as: 'payments', - }, - ], - }, - ], - }; - await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger); + as: 'payments' + } + ] + } + ] + } + await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) await indexBulkDataToES( jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger - ); + ) await indexBulkDataToES( resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger - ); + ) } /** @@ -571,74 +578,74 @@ async function importData(pathToFile, dataModels, logger) { * @param {Array} dataModels the data models to export * @param {Object} logger the logger object */ -async function exportData(pathToFile, dataModels, logger) { +async function exportData (pathToFile, dataModels, logger) { logger.info({ component: 'exportData', - message: `Start Saving data to file with path ${pathToFile}....`, - }); + message: `Start Saving data to file with path ${pathToFile}....` + }) - const allModelsRecords = {}; + const allModelsRecords = {} for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index]; - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); - const modelRecords = await models[modelName].findAll({ include }); - const rawRecords = _.map(modelRecords, (r) => r.toJSON()); - allModelsRecords[modelName] = rawRecords; + const modelOpts = dataModels[index] + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) + const modelRecords = await models[modelName].findAll({ include }) + const rawRecords = _.map(modelRecords, (r) => r.toJSON()) + allModelsRecords[modelName] = rawRecords logger.info({ component: 'exportData', - message: `Records loaded for model: ${modelName} = ${rawRecords.length}`, - }); + message: `Records loaded for model: ${modelName} = ${rawRecords.length}` + }) } - fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)); + fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)) logger.info({ component: 'exportData', - message: 'End Saving data to file....', - }); + message: 'End Saving data to file....' + }) } /** * Format a time in milliseconds into a human readable format * @param {Date} milliseconds the number of milliseconds */ -function formatTime(millisec) { - const ms = Math.floor(millisec % 1000); - const secs = Math.floor((millisec / 1000) % 60); - const mins = Math.floor((millisec / (1000 * 60)) % 60); - const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24); - const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7); - const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4); - const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12); - const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)); - - let formattedTime = '0 milliseconds'; +function formatTime (millisec) { + const ms = Math.floor(millisec % 1000) + const secs = Math.floor((millisec / 1000) % 60) + const mins = Math.floor((millisec / (1000 * 60)) % 60) + const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24) + const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7) + const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4) + const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12) + const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)) + + let formattedTime = '0 milliseconds' if (ms > 0) { - formattedTime = `${ms} milliseconds`; + formattedTime = `${ms} milliseconds` } if (secs > 0) { - formattedTime = `${secs} seconds ${formattedTime}`; + formattedTime = `${secs} seconds ${formattedTime}` } if (mins > 0) { - formattedTime = `${mins} minutes ${formattedTime}`; + formattedTime = `${mins} minutes ${formattedTime}` } if (hrs > 0) { - formattedTime = `${hrs} hours ${formattedTime}`; + formattedTime = `${hrs} hours ${formattedTime}` } if (days > 0) { - formattedTime = `${days} days ${formattedTime}`; + formattedTime = `${days} days ${formattedTime}` } if (weeks > 0) { - formattedTime = `${weeks} weeks ${formattedTime}`; + formattedTime = `${weeks} weeks ${formattedTime}` } if (mnths > 0) { - formattedTime = `${mnths} months ${formattedTime}`; + formattedTime = `${mnths} months ${formattedTime}` } if (yrs > 0) { - formattedTime = `${yrs} years ${formattedTime}`; + formattedTime = `${yrs} years ${formattedTime}` } - return formattedTime.trim(); + return formattedTime.trim() } /** @@ -647,30 +654,30 @@ function formatTime(millisec) { * @param {Array} source the array in which to search for the term * @param {Array | String} term the term to search */ -function checkIfExists(source, term) { - let terms; +function checkIfExists (source, term) { + let terms if (!_.isArray(source)) { - throw new Error('Source argument should be an array'); + throw new Error('Source argument should be an array') } - source = source.map((s) => s.toLowerCase()); + source = source.map((s) => s.toLowerCase()) if (_.isString(term)) { - terms = term.toLowerCase().split(' '); + terms = term.toLowerCase().split(' ') } else if (_.isArray(term)) { - terms = term.map((t) => t.toLowerCase()); + terms = term.map((t) => t.toLowerCase()) } else { - throw new Error('Term argument should be either a string or an array'); + throw new Error('Term argument should be either a string or an array') } for (let i = 0; i < terms.length; i++) { if (source.includes(terms[i])) { - return true; + return true } } - return false; + return false } /** @@ -678,10 +685,10 @@ function checkIfExists(source, term) { * @param {Function} fn the async function * @returns {Function} the wrapped function */ -function wrapExpress(fn) { +function wrapExpress (fn) { return function (req, res, next) { - fn(req, res, next).catch(next); - }; + fn(req, res, next).catch(next) + } } /** @@ -689,20 +696,20 @@ function wrapExpress(fn) { * @param obj the object (controller exports) * @returns {Object|Array} the wrapped object */ -function autoWrapExpress(obj) { +function autoWrapExpress (obj) { if (_.isArray(obj)) { - return obj.map(autoWrapExpress); + return obj.map(autoWrapExpress) } if (_.isFunction(obj)) { if (obj.constructor.name === 'AsyncFunction') { - return wrapExpress(obj); + return wrapExpress(obj) } - return obj; + return obj } _.each(obj, (value, key) => { - obj[key] = autoWrapExpress(value); - }); - return obj; + obj[key] = autoWrapExpress(value) + }) + return obj } /** @@ -711,11 +718,11 @@ function autoWrapExpress(obj) { * @param {Number} page the page number * @returns {String} link for the page */ -function getPageLink(req, page) { - const q = _.assignIn({}, req.query, { page }); +function getPageLink (req, page) { + const q = _.assignIn({}, req.query, { page }) return `${req.protocol}://${req.get('Host')}${req.baseUrl}${ req.path - }?${querystring.stringify(q)}`; + }?${querystring.stringify(q)}` } /** @@ -724,31 +731,31 @@ function getPageLink(req, page) { * @param {Object} res the HTTP response * @param {Object} result the operation result */ -function setResHeaders(req, res, result) { - const totalPages = Math.ceil(result.total / result.perPage); +function setResHeaders (req, res, result) { + const totalPages = Math.ceil(result.total / result.perPage) if (result.page > 1) { - res.set('X-Prev-Page', result.page - 1); + res.set('X-Prev-Page', result.page - 1) } if (result.page < totalPages) { - res.set('X-Next-Page', result.page + 1); + res.set('X-Next-Page', result.page + 1) } - res.set('X-Page', result.page); - res.set('X-Per-Page', result.perPage); - res.set('X-Total', result.total); - res.set('X-Total-Pages', totalPages); + res.set('X-Page', result.page) + res.set('X-Per-Page', result.perPage) + res.set('X-Total', result.total) + res.set('X-Total-Pages', totalPages) // set Link header if (totalPages > 0) { let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink( req, totalPages - )}>; rel="last"`; + )}>; rel="last"` if (result.page > 1) { - link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"`; + link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"` } if (result.page < totalPages) { - link += `, <${getPageLink(req, result.page + 1)}>; rel="next"`; + link += `, <${getPageLink(req, result.page + 1)}>; rel="next"` } - res.set('Link', link); + res.set('Link', link) } } @@ -756,30 +763,30 @@ function setResHeaders(req, res, result) { * Get ES Client * @return {Object} Elastic Host Client Instance */ -function getESClient() { +function getESClient () { if (esClients.client) { - return esClients.client; + return esClients.client } - const host = config.esConfig.HOST; - const cloudId = config.esConfig.ELASTICCLOUD.id; + const host = config.esConfig.HOST + const cloudId = config.esConfig.ELASTICCLOUD.id if (cloudId) { // Elastic Cloud configuration esClients.client = new elasticsearch.Client({ cloud: { - id: cloudId, + id: cloudId }, auth: { username: config.esConfig.ELASTICCLOUD.username, - password: config.esConfig.ELASTICCLOUD.password, - }, - }); + password: config.esConfig.ELASTICCLOUD.password + } + }) } else { esClients.client = new elasticsearch.Client({ - node: host, - }); + node: host + }) } - return esClients.client; + return esClients.client } /* @@ -790,8 +797,8 @@ const getM2MToken = async () => { return await m2m.getMachineToken( config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET - ); -}; + ) +} /* * Function to get M2M token for U-Bahn @@ -801,8 +808,8 @@ const getM2MUbahnToken = async () => { return await m2mForUbahn.getMachineToken( config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET - ); -}; + ) +} /** * Function to encode query string @@ -810,17 +817,17 @@ const getM2MUbahnToken = async () => { * @param {String} nesting the nesting string * @returns {String} query string */ -function encodeQueryString(queryObj, nesting = '') { +function encodeQueryString (queryObj, nesting = '') { const pairs = Object.entries(queryObj).map(([key, val]) => { // Handle the nested, recursive case, where the value to encode is an object itself if (typeof val === 'object') { - return encodeQueryString(val, nesting + `${key}.`); + return encodeQueryString(val, nesting + `${key}.`) } else { // Handle base case, where the value to encode is simply a string. - return [nesting + key, val].map(querystring.escape).join('='); + return [nesting + key, val].map(querystring.escape).join('=') } - }); - return pairs.join('&'); + }) + return pairs.join('&') } /** @@ -828,31 +835,31 @@ function encodeQueryString(queryObj, nesting = '') { * @param {Integer} externalId the legacy user id * @returns {Array} the users found */ -async function listUsersByExternalId(externalId) { +async function listUsersByExternalId (externalId) { // return empty list if externalId is null or undefined if (!!externalId !== true) { - return []; + return [] } - const token = await getM2MUbahnToken(); + const token = await getM2MUbahnToken() const q = { enrich: true, externalProfile: { organizationId: config.ORG_ID, - externalId, - }, - }; - const url = `${config.TC_API}/users?${encodeQueryString(q)}`; + externalId + } + } + const url = `${config.TC_API}/users?${encodeQueryString(q)}` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listUserByExternalId', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -860,14 +867,14 @@ async function listUsersByExternalId(externalId) { * @param {Integer} externalId the legacy user id * @returns {Object} the user */ -async function getUserByExternalId(externalId) { - const users = await listUsersByExternalId(externalId); +async function getUserByExternalId (externalId) { + const users = await listUsersByExternalId(externalId) if (_.isEmpty(users)) { throw new errors.NotFoundError( `externalId: ${externalId} "user" not found` - ); + ) } - return users[0]; + return users[0] } /** @@ -876,24 +883,24 @@ async function getUserByExternalId(externalId) { * @params {Object} payload the payload * @params {Object} options the extra options to control the function */ -async function postEvent(topic, payload, options = {}) { +async function postEvent (topic, payload, options = {}) { logger.debug({ component: 'helper', context: 'postEvent', message: `Posting event to Kafka topic ${topic}, ${JSON.stringify( payload - )}`, - }); - const client = getBusApiClient(); + )}` + }) + const client = getBusApiClient() const message = { topic, originator: config.KAFKA_MESSAGE_ORIGINATOR, timestamp: new Date().toISOString(), 'mime-type': 'application/json', - payload, - }; - await client.postEvent(message); - await eventDispatcher.handleEvent(topic, { value: payload, options }); + payload + } + await client.postEvent(message) + await eventDispatcher.handleEvent(topic, { value: payload, options }) } /** @@ -902,11 +909,11 @@ async function postEvent(topic, payload, options = {}) { * @param {Object} err the err * @returns {Boolean} the result */ -function isDocumentMissingException(err) { +function isDocumentMissingException (err) { if (err.statusCode === 404 && err instanceof ESResponseError) { - return true; + return true } - return false; + return false } /** @@ -915,34 +922,34 @@ function isDocumentMissingException(err) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getProjects(currentUser, criteria = {}) { - let token; +async function getProjects (currentUser, criteria = {}) { + let token if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken(); - token = `Bearer ${m2mToken}`; + const m2mToken = await getM2MToken() + token = `Bearer ${m2mToken}` } else { - token = currentUser.jwtToken; + token = currentUser.jwtToken } - const url = `${config.TC_API}/projects?type=talent-as-a-service`; + const url = `${config.TC_API}/projects?type=talent-as-a-service` const res = await request .get(url) .query(criteria) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getProjects', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) const result = _.map(res.body, (item) => { - return _.pick(item, ['id', 'name', 'invites', 'members']); - }); + return _.pick(item, ['id', 'name', 'invites', 'members']) + }) return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result, - }; + result + } } /** @@ -951,24 +958,24 @@ async function getProjects(currentUser, criteria = {}) { * @param {String} userId the legacy user id * @returns {Object} the user */ -async function getTopcoderUserById(userId) { - const token = await getM2MToken(); +async function getTopcoderUserById (userId) { + const token = await getM2MToken() const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `id=${userId}` }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getTopcoderUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - const user = _.get(res.body, 'result.content[0]'); + message: `response body: ${JSON.stringify(res.body)}` + }) + const user = _.get(res.body, 'result.content[0]') if (!user) { throw new errors.NotFoundError( `userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}` - ); + ) } - return user; + return user } /** @@ -976,31 +983,31 @@ async function getTopcoderUserById(userId) { * @param {String} userId the user id * @returns the request result */ -async function getUserById(userId, enrich) { - const token = await getM2MUbahnToken(); +async function getUserById (userId, enrich) { + const token = await getM2MUbahnToken() const res = await request .get(`${config.TC_API}/users/${userId}` + (enrich ? '?enrich=true' : '')) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) - const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']); + const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']) if (enrich) { user.skills = (res.body.skills || []).map((skillObj) => _.pick(skillObj.skill, ['id', 'name']) - ); - const attributes = _.get(res, 'body.attributes', []); + ) + const attributes = _.get(res, 'body.attributes', []) user.attributes = _.map(attributes, (attr) => _.pick(attr, ['id', 'value', 'attribute.id', 'attribute.name']) - ); + ) } - return user; + return user } /** @@ -1008,19 +1015,19 @@ async function getUserById(userId, enrich) { * @param {Object} data the user data * @returns the request result */ -async function createUbahnUser({ handle, firstName, lastName }) { - const token = await getM2MUbahnToken(); +async function createUbahnUser ({ handle, firstName, lastName }) { + const token = await getM2MUbahnToken() const res = await request .post(`${config.TC_API}/users`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ handle, firstName, lastName }); + .send({ handle, firstName, lastName }) localLogger.debug({ context: 'createUbahnUser', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id']) } /** @@ -1028,21 +1035,21 @@ async function createUbahnUser({ handle, firstName, lastName }) { * @param {String} userId the user id(with uuid format) * @param {Object} data the profile data */ -async function createUserExternalProfile( +async function createUserExternalProfile ( userId, { organizationId, externalId } ) { - const token = await getM2MUbahnToken(); + const token = await getM2MUbahnToken() const res = await request .post(`${config.TC_API}/users/${userId}/externalProfiles`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ organizationId, externalId: String(externalId) }); + .send({ organizationId, externalId: String(externalId) }) localLogger.debug({ context: 'createUserExternalProfile', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) } /** @@ -1050,23 +1057,23 @@ async function createUserExternalProfile( * @param {Array} handles the handle array * @returns the request result */ -async function getMembers(handles) { - const token = await getM2MToken(); +async function getMembers (handles) { + const token = await getM2MToken() const handlesStr = _.map(handles, (handle) => { - return '%22' + handle.toLowerCase() + '%22'; - }).join(','); - const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]`; + return '%22' + handle.toLowerCase() + '%22' + }).join(',') + const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getMembers', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -1075,36 +1082,36 @@ async function getMembers(handles) { * @param {Number} id project id * @returns the request result */ -async function getProjectById(currentUser, id) { - let token; +async function getProjectById (currentUser, id) { + let token if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken(); - token = `Bearer ${m2mToken}`; + const m2mToken = await getM2MToken() + token = `Bearer ${m2mToken}` } else { - token = currentUser.jwtToken; + token = currentUser.jwtToken } - const url = `${config.TC_API}/projects/${id}`; + const url = `${config.TC_API}/projects/${id}` try { const res = await request .get(url) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getProjectById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id', 'name', 'invites', 'members']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id', 'name', 'invites', 'members']) } catch (err) { if (err.status === HttpStatus.FORBIDDEN) { throw new errors.ForbiddenError( `You are not allowed to access the project with id ${id}` - ); + ) } if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${id} project not found`); + throw new errors.NotFoundError(`id: ${id} project not found`) } - throw err; + throw err } } @@ -1115,33 +1122,33 @@ async function getProjectById(currentUser, id) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getTopcoderSkills(criteria) { - const token = await getM2MUbahnToken(); +async function getTopcoderSkills (criteria) { + const token = await getM2MUbahnToken() try { const res = await request .get(`${config.TC_API}/skills`) .query({ skillProviderId: config.TOPCODER_SKILL_PROVIDER_ID, - ...criteria, + ...criteria }) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getTopcoderSkills', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result: res.body, - }; + result: res.body + } } catch (err) { if (err.status === HttpStatus.BAD_REQUEST) { - throw new errors.BadRequestError(err.response.body.message); + throw new errors.BadRequestError(err.response.body.message) } - throw err; + throw err } } @@ -1150,18 +1157,18 @@ async function getTopcoderSkills(criteria) { * @param {String} skillId the skill Id * @returns the request result */ -async function getSkillById(skillId) { - const token = await getM2MUbahnToken(); +async function getSkillById (skillId) { + const token = await getM2MUbahnToken() const res = await request .get(`${config.TC_API}/skills/${skillId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getSkillById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id', 'name']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id', 'name']) } /** @@ -1174,22 +1181,22 @@ async function getSkillById(skillId) { * @params {Object} currentUser the user who perform this operation * @returns {String} the ubahn user id */ -async function ensureUbahnUserId(currentUser) { +async function ensureUbahnUserId (currentUser) { try { - return (await getUserByExternalId(currentUser.userId)).id; + return (await getUserByExternalId(currentUser.userId)).id } catch (err) { if (!(err instanceof errors.NotFoundError)) { - throw err; + throw err } - const topcoderUser = await getTopcoderUserById(currentUser.userId); + const topcoderUser = await getTopcoderUserById(currentUser.userId) const user = await createUbahnUser( _.pick(topcoderUser, ['handle', 'firstName', 'lastName']) - ); + ) await createUserExternalProfile(user.id, { organizationId: config.ORG_ID, - externalId: currentUser.userId, - }); - return user.id; + externalId: currentUser.userId + }) + return user.id } } @@ -1199,8 +1206,8 @@ async function ensureUbahnUserId(currentUser) { * @param {String} jobId the job id * @returns {Object} the job data */ -async function ensureJobById(jobId) { - return models.Job.findById(jobId); +async function ensureJobById (jobId) { + return models.Job.findById(jobId) } /** @@ -1209,8 +1216,8 @@ async function ensureJobById(jobId) { * @param {String} resourceBookingId the resourceBooking id * @returns {Object} the resourceBooking data */ -async function ensureResourceBookingById(resourceBookingId) { - return models.ResourceBooking.findById(resourceBookingId); +async function ensureResourceBookingById (resourceBookingId) { + return models.ResourceBooking.findById(resourceBookingId) } /** @@ -1218,8 +1225,8 @@ async function ensureResourceBookingById(resourceBookingId) { * @param {String} workPeriodId the workPeriod id * @returns the workPeriod data */ -async function ensureWorkPeriodById(workPeriodId) { - return models.WorkPeriod.findById(workPeriodId); +async function ensureWorkPeriodById (workPeriodId) { + return models.WorkPeriod.findById(workPeriodId) } /** @@ -1228,24 +1235,24 @@ async function ensureWorkPeriodById(workPeriodId) { * @param {String} jobId the user id * @returns {Object} the user data */ -async function ensureUserById(userId) { - const token = await getM2MUbahnToken(); +async function ensureUserById (userId) { + const token = await getM2MUbahnToken() try { const res = await request .get(`${config.TC_API}/users/${userId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'ensureUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${userId} "user" not found`); + throw new errors.NotFoundError(`id: ${userId} "user" not found`) } - throw err; + throw err } } @@ -1254,12 +1261,12 @@ async function ensureUserById(userId) { * * @returns {Object} the M2M auth user */ -function getAuditM2Muser() { +function getAuditM2Muser () { return { isMachine: true, userId: config.m2m.M2M_AUDIT_USER_ID, - handle: config.m2m.M2M_AUDIT_HANDLE, - }; + handle: config.m2m.M2M_AUDIT_HANDLE + } } /** @@ -1271,24 +1278,24 @@ function getAuditM2Muser() { * @param {Number} projectId project id * @returns the result */ -async function checkIsMemberOfProject(userId, projectId) { - const m2mToken = await getM2MToken(); +async function checkIsMemberOfProject (userId, projectId) { + const m2mToken = await getM2MToken() const res = await request .get(`${config.TC_API}/projects/${projectId}`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const memberIdList = _.map(res.body.members, 'userId'); + .set('Accept', 'application/json') + const memberIdList = _.map(res.body.members, 'userId') localLogger.debug({ context: 'checkIsMemberOfProject', message: `the members of project ${projectId}: ${JSON.stringify( memberIdList - )}, authUserId: ${JSON.stringify(userId)}`, - }); + )}, authUserId: ${JSON.stringify(userId)}` + }) if (!memberIdList.includes(userId)) { throw new errors.UnauthorizedError( `userId: ${userId} the user is not a member of project ${projectId}` - ); + ) } } @@ -1298,24 +1305,24 @@ async function checkIsMemberOfProject(userId, projectId) { * @param {Array} handles the array of handles * @returns {Array} the member details */ -async function getMemberDetailsByHandles(handles) { +async function getMemberDetailsByHandles (handles) { if (!handles.length) { - return []; + return [] } - const token = await getM2MToken(); + const token = await getM2MToken() const res = await request .get(`${config.TOPCODER_MEMBERS_API}/`) .query({ 'handlesLower[]': handles.map(handle => handle.toLowerCase()), - fields: 'userId,handle,handleLower,firstName,lastName,email', + fields: 'userId,handle,handleLower,firstName,lastName,email' }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getMemberDetailsByHandles', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -1324,7 +1331,7 @@ async function getMemberDetailsByHandles(handles) { * @param {String} handle the user handle * @returns {Object} the member details */ -async function getMemberDetailsByHandle(handle) { +async function getMemberDetailsByHandle (handle) { const [memberDetails] = await getMemberDetailsByHandles([handle]) if (!memberDetails) { @@ -1341,20 +1348,20 @@ async function getMemberDetailsByHandle(handle) { * @param {String} email the email * @returns {Array} the member details */ -async function _getMemberDetailsByEmail(token, email) { +async function _getMemberDetailsByEmail (token, email) { const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `email=${email}`, - fields: 'handle,id,email,firstName,lastName', + fields: 'handle,id,email,firstName,lastName' }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: '_getMemberDetailsByEmail', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res.body, 'result.content'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res.body, 'result.content') } /** @@ -1364,25 +1371,25 @@ async function _getMemberDetailsByEmail(token, email) { * @param {Array} emails the array of emails * @returns {Array} the member details */ -async function getMemberDetailsByEmails(emails) { - const token = await getM2MToken(); +async function getMemberDetailsByEmails (emails) { + const token = await getM2MToken() const limiter = new Bottleneck({ - maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API, - }); + maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API + }) const membersArray = await Promise.all( emails.map((email) => limiter.schedule(() => _getMemberDetailsByEmail(token, email).catch((error) => { localLogger.error({ context: 'getMemberDetailsByEmails', - message: error.message, - }); - return []; + message: error.message + }) + return [] }) ) ) - ); - return _.flatten(membersArray); + ) + return _.flatten(membersArray) } /** @@ -1393,20 +1400,20 @@ async function getMemberDetailsByEmails(emails) { * @param {Object} criteria the filtering criteria * @returns {Object} the member created */ -async function createProjectMember(projectId, data, criteria) { - const m2mToken = await getM2MToken(); +async function createProjectMember (projectId, data, criteria) { + const m2mToken = await getM2MToken() const { body: member } = await request .post(`${config.TC_API}/projects/${projectId}/members`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') .query(criteria) - .send(data); + .send(data) localLogger.debug({ context: 'createProjectMember', - message: `response body: ${JSON.stringify(member)}`, - }); - return member; + message: `response body: ${JSON.stringify(member)}` + }) + return member } /** @@ -1416,21 +1423,21 @@ async function createProjectMember(projectId, data, criteria) { * @param {Object} criteria the search criteria * @returns {Array} the project members */ -async function listProjectMembers(currentUser, projectId, criteria = {}) { +async function listProjectMembers (currentUser, projectId, criteria = {}) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken const { body: members } = await request .get(`${config.TC_API}/projects/${projectId}/members`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listProjectMembers', - message: `response body: ${JSON.stringify(members)}`, - }); - return members; + message: `response body: ${JSON.stringify(members)}` + }) + return members } /** @@ -1440,21 +1447,21 @@ async function listProjectMembers(currentUser, projectId, criteria = {}) { * @param {Object} criteria the search criteria * @returns {Array} the member invites */ -async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { +async function listProjectMemberInvites (currentUser, projectId, criteria = {}) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken const { body: invites } = await request .get(`${config.TC_API}/projects/${projectId}/invites`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listProjectMemberInvites', - message: `response body: ${JSON.stringify(invites)}`, - }); - return invites; + message: `response body: ${JSON.stringify(invites)}` + }) + return invites } /** @@ -1464,24 +1471,24 @@ async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { * @param {String} projectMemberId the id of the project member * @returns {undefined} */ -async function deleteProjectMember(currentUser, projectId, projectMemberId) { +async function deleteProjectMember (currentUser, projectId, projectMemberId) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken try { await request .delete( `${config.TC_API}/projects/${projectId}/members/${projectMemberId}` ) - .set('Authorization', token); + .set('Authorization', token) } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { throw new errors.NotFoundError( `projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}` - ); + ) } - throw err; + throw err } } @@ -1491,13 +1498,13 @@ async function deleteProjectMember(currentUser, projectId, projectMemberId) { * @param {String} attributeName Requested attribute name, e.g. "email" * @returns attribute value */ -function getUserAttributeValue(user, attributeName) { - const attributes = _.get(user, 'attributes', []); +function getUserAttributeValue (user, attributeName) { + const attributes = _.get(user, 'attributes', []) const targetAttribute = _.find( attributes, (a) => a.attribute.name === attributeName - ); - return _.get(targetAttribute, 'value'); + ) + return _.get(targetAttribute, 'value') } /** @@ -1507,34 +1514,34 @@ function getUserAttributeValue(user, attributeName) { * @param {String} token m2m token * @returns {Object} the challenge created */ -async function createChallenge(data, token) { +async function createChallenge (data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/challenges`; + const url = `${config.TC_API}/challenges` localLogger.debug({ context: 'createChallenge', - message: `EndPoint: POST ${url}`, - }); + message: `EndPoint: POST ${url}` + }) localLogger.debug({ context: 'createChallenge', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: challenge, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createChallenge', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'createChallenge', - message: `Response Body: ${JSON.stringify(challenge)}`, - }); - return challenge; + message: `Response Body: ${JSON.stringify(challenge)}` + }) + return challenge } /** @@ -1545,34 +1552,34 @@ async function createChallenge(data, token) { * @param {String} token m2m token * @returns {Object} the challenge updated */ -async function updateChallenge(challengeId, data, token) { +async function updateChallenge (challengeId, data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/challenges/${challengeId}`; + const url = `${config.TC_API}/challenges/${challengeId}` localLogger.debug({ context: 'updateChallenge', - message: `EndPoint: PATCH ${url}`, - }); + message: `EndPoint: PATCH ${url}` + }) localLogger.debug({ context: 'updateChallenge', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: challenge, status: httpStatus } = await request .patch(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'updateChallenge', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'updateChallenge', - message: `Response Body: ${JSON.stringify(challenge)}`, - }); - return challenge; + message: `Response Body: ${JSON.stringify(challenge)}` + }) + return challenge } /** @@ -1582,34 +1589,34 @@ async function updateChallenge(challengeId, data, token) { * @param {String} token m2m token * @returns {Object} the resource created */ -async function createChallengeResource(data, token) { +async function createChallengeResource (data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/resources`; + const url = `${config.TC_API}/resources` localLogger.debug({ context: 'createChallengeResource', - message: `EndPoint: POST ${url}`, - }); + message: `EndPoint: POST ${url}` + }) localLogger.debug({ context: 'createChallengeResource', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: resource, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createChallengeResource', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'createChallengeResource', - message: `Response Body: ${JSON.stringify(resource)}`, - }); - return resource; + message: `Response Body: ${JSON.stringify(resource)}` + }) + return resource } /** @@ -1618,40 +1625,40 @@ async function createChallengeResource(data, token) { * @param {Date} end end date of the resource booking * @returns {Array<{startDate:Date, endDate:Date, daysWorked:number}>} information about workPeriods */ -function extractWorkPeriods(start, end) { +function extractWorkPeriods (start, end) { // calculate maximum possible daysWorked for a week - function getDaysWorked(week) { + function getDaysWorked (week) { if (weeks === 1) { - return Math.min(endDay, 5) - Math.max(startDay, 1) + 1; + return Math.min(endDay, 5) - Math.max(startDay, 1) + 1 } else if (week === 0) { - return Math.min(6 - startDay, 5); + return Math.min(6 - startDay, 5) } else if (week === weeks - 1) { - return Math.min(endDay, 5); - } else return 5; + return Math.min(endDay, 5) + } else return 5 } - const periods = []; + const periods = [] if (_.isNil(start) || _.isNil(end)) { - return periods; + return periods } - const startDate = moment(start); - const startDay = startDate.get('day'); - startDate.set('day', 0).startOf('day'); + const startDate = moment(start) + const startDay = startDate.get('day') + startDate.set('day', 0).startOf('day') - const endDate = moment(end); - const endDay = endDate.get('day'); - endDate.set('day', 6).endOf('day'); + const endDate = moment(end) + const endDay = endDate.get('day') + endDate.set('day', 6).endOf('day') - const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7; + const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7 for (let i = 0; i < weeks; i++) { periods.push({ startDate: startDate.format('YYYY-MM-DD'), endDate: startDate.add(6, 'day').format('YYYY-MM-DD'), - daysWorked: getDaysWorked(i), - }); - startDate.add(1, 'day'); + daysWorked: getDaysWorked(i) + }) + startDate.add(1, 'day') } - return periods; + return periods } /** @@ -1660,19 +1667,19 @@ function extractWorkPeriods(start, end) { * @param {String} userHandle user handle * @returns {String} email address of the user */ -async function getUserByHandle(userHandle) { - const token = await getM2MToken(); - const url = `${config.TC_API}/members/${userHandle}`; +async function getUserByHandle (userHandle) { + const token = await getM2MToken() + const url = `${config.TC_API}/members/${userHandle}` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getUserByHandle', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res, 'body'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res, 'body') } /** @@ -1681,14 +1688,14 @@ async function getUserByHandle(userHandle) { * @param {*} object of json that would be replaced in string * @returns */ -async function substituteStringByObject(string, object) { +async function substituteStringByObject (string, object) { for (var key in object) { if (!Object.prototype.hasOwnProperty.call(object, key)) { - continue; + continue } - string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]); + string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]) } - return string; + return string } /** @@ -1696,19 +1703,19 @@ async function substituteStringByObject(string, object) { * @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 (currentUser, data) { + const token = currentUser.jwtToken const res = await request .post(`${config.TC_API}/projects/`) .set('Authorization', token) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createProject', - message: `response body: ${JSON.stringify(res)}`, - }); - return _.get(res, 'body'); + message: `response body: ${JSON.stringify(res)}` + }) + return _.get(res, 'body') } module.exports = { @@ -1727,9 +1734,9 @@ module.exports = { getUserId: async (userId) => { // check m2m user id if (userId === config.m2m.M2M_AUDIT_USER_ID) { - return config.m2m.M2M_AUDIT_USER_ID; + return config.m2m.M2M_AUDIT_USER_ID } - return ensureUbahnUserId({ userId }); + return ensureUbahnUserId({ userId }) }, getUserByExternalId, getM2MToken, @@ -1763,5 +1770,5 @@ module.exports = { extractWorkPeriods, getUserByHandle, substituteStringByObject, - createProject, -}; + createProject +} diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index ca4f1bca..26d70738 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -1,19 +1,19 @@ /** * Controller for TaaS teams endpoints */ -const HttpStatus = require('http-status-codes'); -const service = require('../services/TeamService'); -const helper = require('../common/helper'); +const HttpStatus = require('http-status-codes') +const service = require('../services/TeamService') +const helper = require('../common/helper') /** * Search teams * @param req the request * @param res the response */ -async function searchTeams(req, res) { - const result = await service.searchTeams(req.authUser, req.query); - helper.setResHeaders(req, res, result); - res.send(result.result); +async function searchTeams (req, res) { + const result = await service.searchTeams(req.authUser, req.query) + helper.setResHeaders(req, res, result) + res.send(result.result) } /** @@ -21,8 +21,8 @@ async function searchTeams(req, res) { * @param req the request * @param res the response */ -async function getTeam(req, res) { - res.send(await service.getTeam(req.authUser, req.params.id)); +async function getTeam (req, res) { + res.send(await service.getTeam(req.authUser, req.params.id)) } /** @@ -30,10 +30,10 @@ async function getTeam(req, res) { * @param req the request * @param res the response */ -async function getTeamJob(req, res) { +async function getTeamJob (req, res) { res.send( await service.getTeamJob(req.authUser, req.params.id, req.params.jobId) - ); + ) } /** @@ -41,9 +41,9 @@ async function getTeamJob(req, res) { * @param req the request * @param res the response */ -async function sendEmail(req, res) { - await service.sendEmail(req.authUser, req.body); - res.status(HttpStatus.NO_CONTENT).end(); +async function sendEmail (req, res) { + await service.sendEmail(req.authUser, req.body) + res.status(HttpStatus.NO_CONTENT).end() } /** @@ -51,10 +51,10 @@ async function sendEmail(req, res) { * @param req the request * @param res the response */ -async function addMembers(req, res) { +async function addMembers (req, res) { res.send( await service.addMembers(req.authUser, req.params.id, req.query, req.body) - ); + ) } /** @@ -62,13 +62,13 @@ async function addMembers(req, res) { * @param req the request * @param res the response */ -async function searchMembers(req, res) { +async function searchMembers (req, res) { const result = await service.searchMembers( req.authUser, req.params.id, req.query - ); - res.send(result.result); + ) + res.send(result.result) } /** @@ -76,13 +76,13 @@ async function searchMembers(req, res) { * @param req the request * @param res the response */ -async function searchInvites(req, res) { +async function searchInvites (req, res) { const result = await service.searchInvites( req.authUser, req.params.id, req.query - ); - res.send(result.result); + ) + res.send(result.result) } /** @@ -90,13 +90,13 @@ async function searchInvites(req, res) { * @param req the request * @param res the response */ -async function deleteMember(req, res) { +async function deleteMember (req, res) { await service.deleteMember( req.authUser, req.params.id, req.params.projectMemberId - ); - res.status(HttpStatus.NO_CONTENT).end(); + ) + res.status(HttpStatus.NO_CONTENT).end() } /** @@ -104,8 +104,8 @@ async function deleteMember(req, res) { * @param req the request * @param res the response */ -async function getMe(req, res) { - res.send(await service.getMe(req.authUser)); +async function getMe (req, res) { + res.send(await service.getMe(req.authUser)) } /** @@ -113,8 +113,8 @@ async function getMe(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 createProj (req, res) { + res.send(await service.createProj(req.authUser, req.body)) } module.exports = { @@ -127,5 +127,5 @@ module.exports = { searchInvites, deleteMember, getMe, - createProj, -}; + createProj +} diff --git a/src/models/Job.js b/src/models/Job.js index 49d34ff7..477327f6 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -104,6 +104,36 @@ module.exports = (sequelize) => { defaultValue: false, allowNull: false }, + minSalary: { + field: 'min_salary', + type: Sequelize.INTEGER, + allowNull: false + }, + maxSalary: { + field: 'max_salary', + type: Sequelize.INTEGER, + allowNull: false + }, + hoursPerWeek: { + field: 'hours_per_week', + type: Sequelize.INTEGER, + allowNull: false + }, + jobLocation: { + field: 'job_location', + type: Sequelize.STRING(255), + allowNull: false + }, + jobTimezone: { + field: 'job_timezone', + type: Sequelize.STRING(128), + allowNull: false + }, + currency: { + field: 'currency', + type: Sequelize.STRING(30), + allowNull: false + }, createdBy: { field: 'created_by', type: Sequelize.UUID, diff --git a/src/models/JobCandidate.js b/src/models/JobCandidate.js index 819b9177..fc54c0aa 100644 --- a/src/models/JobCandidate.js +++ b/src/models/JobCandidate.js @@ -62,6 +62,9 @@ module.exports = (sequelize) => { resume: { type: Sequelize.STRING(2048) }, + remark: { + type: Sequelize.STRING(255) + }, createdBy: { field: 'created_by', type: Sequelize.UUID, diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index 9bbe25c6..07d777d4 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -1,7 +1,7 @@ /** * Contains taas team routes */ -const constants = require('../../app-constants'); +const constants = require('../../app-constants') module.exports = { '/taas-teams': { @@ -9,85 +9,85 @@ module.exports = { controller: 'TeamController', method: 'searchTeams', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/email': { post: { controller: 'TeamController', method: 'sendEmail', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/skills': { get: { controller: 'SkillController', method: 'searchSkills', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/me': { get: { controller: 'TeamController', method: 'getMe', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id': { get: { controller: 'TeamController', method: 'getTeam', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/jobs/:jobId': { get: { controller: 'TeamController', method: 'getTeamJob', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/members': { post: { controller: 'TeamController', method: 'addMembers', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], + scopes: [constants.Scopes.READ_TAAS_TEAM] }, get: { controller: 'TeamController', method: 'searchMembers', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/invites': { get: { controller: 'TeamController', method: 'searchInvites', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/members/:projectMemberId': { delete: { controller: 'TeamController', method: 'deleteMember', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/createTeamRequest': { post: { controller: 'TeamController', method: 'createProj', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, - }, -}; + scopes: [constants.Scopes.READ_TAAS_TEAM] + } + } +} diff --git a/src/services/InterviewService.js b/src/services/InterviewService.js index 10a065f4..a69a788c 100644 --- a/src/services/InterviewService.js +++ b/src/services/InterviewService.js @@ -241,8 +241,8 @@ async function requestInterview (currentUser, jobCandidateId, interview) { 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] + var foundGuestMember = _.find(guestMembers, function (guestMember) { return guestEmail === guestMember.email }) + return (foundGuestMember !== undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split('@')[0] }) try { diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index 392d0af6..cc059c0c 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -130,7 +130,8 @@ createJobCandidate.schema = Joi.object().keys({ jobId: Joi.string().uuid().required(), userId: Joi.string().uuid().required(), externalId: Joi.string().allow(null), - resume: Joi.string().uri().allow(null) + resume: Joi.string().uri().allow(null), + remark: Joi.string().allow(null) }).required() }).required() @@ -176,7 +177,8 @@ partiallyUpdateJobCandidate.schema = Joi.object().keys({ data: Joi.object().keys({ status: Joi.jobCandidateStatus(), externalId: Joi.string().allow(null), - resume: Joi.string().uri().allow(null) + resume: Joi.string().uri().allow(null), + remark: Joi.string().allow(null) }).required() }).required() @@ -201,7 +203,8 @@ fullyUpdateJobCandidate.schema = Joi.object().keys({ userId: Joi.string().uuid().required(), status: Joi.jobCandidateStatus().default('open'), externalId: Joi.string().allow(null).default(null), - resume: Joi.string().uri().allow(null).default(null) + resume: Joi.string().uri().allow(null).default(null), + remark: Joi.string().allow(null).default(null) }).required() }).required() diff --git a/src/services/JobService.js b/src/services/JobService.js index 61685901..09b48d71 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -177,7 +177,13 @@ createJob.schema = Joi.object().keys({ rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()).required(), - isApplicationPageActive: Joi.boolean() + isApplicationPageActive: Joi.boolean(), + minSalary: Joi.number().integer().required(), + maxSalary: Joi.number().integer().required(), + hoursPerWeek: Joi.number().integer().required(), + jobLocation: Joi.string().required(), + jobTimezone: Joi.string().required(), + currency: Joi.string().required() }).required() }).required() @@ -245,7 +251,13 @@ partiallyUpdateJob.schema = Joi.object().keys({ rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()), - isApplicationPageActive: Joi.boolean() + isApplicationPageActive: Joi.boolean(), + minSalary: Joi.number().integer(), + maxSalary: Joi.number().integer(), + hoursPerWeek: Joi.number().integer(), + jobLocation: Joi.string(), + jobTimezone: Joi.string(), + currency: Joi.string() }).required() }).required() @@ -276,7 +288,13 @@ fullyUpdateJob.schema = Joi.object().keys({ workload: Joi.workload().allow(null).default(null), skills: Joi.array().items(Joi.string().uuid()).required(), status: Joi.jobStatus().default('sourcing'), - isApplicationPageActive: Joi.boolean() + isApplicationPageActive: Joi.boolean(), + minSalary: Joi.number().integer().required(), + maxSalary: Joi.number().integer().required(), + hoursPerWeek: Joi.number().integer().required(), + jobLocation: Joi.string().required(), + jobTimezone: Joi.string().required(), + currency: Joi.string().required() }).required() }).required() diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index f5c40206..fd3d7773 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unreachable */ /** * This service provides operations of ResourceBooking. */ diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 3f6dbfd3..4052e942 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -2,16 +2,16 @@ * This service provides operations of Job. */ -const _ = require('lodash'); -const Joi = require('joi'); -const dateFNS = require('date-fns'); -const config = require('config'); -const emailTemplateConfig = require('../../config/email_template.config'); -const helper = require('../common/helper'); -const logger = require('../common/logger'); -const errors = require('../common/errors'); -const JobService = require('./JobService'); -const ResourceBookingService = require('./ResourceBookingService'); +const _ = require('lodash') +const Joi = require('joi') +const dateFNS = require('date-fns') +const config = require('config') +const emailTemplateConfig = require('../../config/email_template.config') +const helper = require('../common/helper') +const logger = require('../common/logger') +const errors = require('../common/errors') +const JobService = require('./JobService') +const ResourceBookingService = require('./ResourceBookingService') const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { return { @@ -20,9 +20,9 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { from: template.from, recipients: template.recipients, cc: template.cc, - sendgridTemplateId: template.sendgridTemplateId, - }; -}); + sendgridTemplateId: template.sendgridTemplateId + } +}) /** * Function to get placed resource bookings with specific projectIds @@ -30,14 +30,14 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { * @param {Array} projectIds project ids * @returns the request result */ -async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { - const criteria = { status: 'placed', projectIds }; +async function _getPlacedResourceBookingsByProjectIds (currentUser, projectIds) { + const criteria = { status: 'placed', projectIds } const { result } = await ResourceBookingService.searchResourceBookings( currentUser, criteria, { returnAll: true } - ); - return result; + ) + return result } /** @@ -46,13 +46,13 @@ async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { * @param {Array} projectIds project ids * @returns the request result */ -async function _getJobsByProjectIds(currentUser, projectIds) { +async function _getJobsByProjectIds (currentUser, projectIds) { const { result } = await JobService.searchJobs( currentUser, { projectIds }, { returnAll: true } - ); - return result; + ) + return result } /** @@ -61,26 +61,26 @@ async function _getJobsByProjectIds(currentUser, projectIds) { * @param {Object} criteria the search criteria * @returns {Object} the search result, contain total/page/perPage and result array */ -async function searchTeams(currentUser, criteria) { - const sort = `${criteria.sortBy} ${criteria.sortOrder}`; +async function searchTeams (currentUser, criteria) { + const sort = `${criteria.sortBy} ${criteria.sortOrder}` // Get projects from /v5/projects with searching criteria const { total, page, perPage, - result: projects, + result: projects } = await helper.getProjects(currentUser, { page: criteria.page, perPage: criteria.perPage, name: criteria.name, - sort, - }); + sort + }) return { total, page, perPage, - result: await getTeamDetail(currentUser, projects), - }; + result: await getTeamDetail(currentUser, projects) + } } searchTeams.schema = Joi.object() @@ -107,13 +107,13 @@ searchTeams.schema = Joi.object() then: Joi.forbidden().label( 'sortOrder(with sortBy being `best match`)' ), - otherwise: Joi.string().valid('asc', 'desc').default('desc'), + otherwise: Joi.string().valid('asc', 'desc').default('desc') }), - name: Joi.string(), + name: Joi.string() }) - .required(), + .required() }) - .required(); + .required() /** * Get team details @@ -122,69 +122,69 @@ searchTeams.schema = Joi.object() * @param {Object} isSearch the flag whether for search function * @returns {Object} the search result */ -async function getTeamDetail(currentUser, projects, isSearch = true) { - const projectIds = _.map(projects, 'id'); +async function getTeamDetail (currentUser, projects, isSearch = true) { + const projectIds = _.map(projects, 'id') // Get all placed resourceBookings filtered by projectIds const resourceBookings = await _getPlacedResourceBookingsByProjectIds( currentUser, projectIds - ); + ) // Get all jobs filtered by projectIds - const jobs = await _getJobsByProjectIds(currentUser, projectIds); + const jobs = await _getJobsByProjectIds(currentUser, projectIds) // Get first week day and last week day - const curr = new Date(); - const firstDay = dateFNS.startOfWeek(curr); - const lastDay = dateFNS.endOfWeek(curr); + const curr = new Date() + const firstDay = dateFNS.startOfWeek(curr) + const lastDay = dateFNS.endOfWeek(curr) logger.debug({ component: 'TeamService', context: 'getTeamDetail', - message: `week started: ${firstDay}, week ended: ${lastDay}`, - }); + message: `week started: ${firstDay}, week ended: ${lastDay}` + }) - const result = []; + const result = [] for (const project of projects) { - const rbs = _.filter(resourceBookings, { projectId: project.id }); - const res = _.clone(project); - res.weeklyCost = 0; - res.resources = []; + const rbs = _.filter(resourceBookings, { projectId: project.id }) + const res = _.clone(project) + res.weeklyCost = 0 + res.resources = [] if (rbs && rbs.length > 0) { // Get minimal start date and maximal end date - const startDates = []; - const endDates = []; + const startDates = [] + const endDates = [] for (const rbsItem of rbs) { if (rbsItem.startDate) { - startDates.push(new Date(rbsItem.startDate)); + startDates.push(new Date(rbsItem.startDate)) } if (rbsItem.endDate) { - endDates.push(new Date(rbsItem.endDate)); + endDates.push(new Date(rbsItem.endDate)) } } if (startDates && startDates.length > 0) { - res.startDate = _.min(startDates); + res.startDate = _.min(startDates) } if (endDates && endDates.length > 0) { - res.endDate = _.max(endDates); + res.endDate = _.max(endDates) } // Count weekly rate for (const item of rbs) { // ignore any resourceBooking that has customerRate missed if (!item.customerRate) { - continue; + continue } - const startDate = new Date(item.startDate); - const endDate = new Date(item.endDate); + const startDate = new Date(item.startDate) + const endDate = new Date(item.endDate) // normally startDate is smaller than endDate for a resourceBooking so not check if startDate < endDate if ( (!item.startDate || startDate < lastDay) && (!item.endDate || endDate > firstDay) ) { - res.weeklyCost += item.customerRate; + res.weeklyCost += item.customerRate } } @@ -194,48 +194,48 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { const resource = { id: rb.id, userId: user.id, - ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']), - }; + ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']) + } // If call function is not search, add jobId field if (!isSearch) { - resource.jobId = rb.jobId; - resource.customerRate = rb.customerRate; - resource.startDate = rb.startDate; - resource.endDate = rb.endDate; + resource.jobId = rb.jobId + resource.customerRate = rb.customerRate + resource.startDate = rb.startDate + resource.endDate = rb.endDate } - return resource; - }); + return resource + }) }) - ); + ) if (resourceInfos && resourceInfos.length > 0) { - res.resources = resourceInfos; + res.resources = resourceInfos - const userHandles = _.map(resourceInfos, 'handle'); + const userHandles = _.map(resourceInfos, 'handle') // Get user photo from /v5/members - const members = await helper.getMembers(userHandles); + const members = await helper.getMembers(userHandles) for (const item of res.resources) { const findMember = _.find(members, { - handleLower: item.handle.toLowerCase(), - }); + handleLower: item.handle.toLowerCase() + }) if (findMember && findMember.photoURL) { - item.photo_url = findMember.photoURL; + item.photo_url = findMember.photoURL } } } } - const jobsTmp = _.filter(jobs, { projectId: project.id }); + const jobsTmp = _.filter(jobs, { projectId: project.id }) if (jobsTmp && jobsTmp.length > 0) { if (isSearch) { // Count total positions - res.totalPositions = 0; + res.totalPositions = 0 for (const item of jobsTmp) { // only sum numPositions of jobs whose status is NOT cancelled or closed if (['cancelled', 'closed'].includes(item.status)) { - continue; + continue } - res.totalPositions += item.numPositions; + res.totalPositions += item.numPositions } } else { res.jobs = _.map(jobsTmp, (job) => { @@ -249,15 +249,15 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { 'skills', 'customerRate', 'status', - 'title', - ]); - }); + 'title' + ]) + }) } } - result.push(res); + result.push(res) } - return result; + return result } /** @@ -266,35 +266,35 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { * @param {String} id the job id * @returns {Object} the team */ -async function getTeam(currentUser, id) { - const project = await helper.getProjectById(currentUser, id); - const result = await getTeamDetail(currentUser, [project], false); - const teamDetail = result[0]; +async function getTeam (currentUser, id) { + const project = await helper.getProjectById(currentUser, id) + const result = await getTeamDetail(currentUser, [project], false) + const teamDetail = result[0] // add job skills for result - let jobSkills = []; + let jobSkills = [] if (teamDetail && teamDetail.jobs) { for (const job of teamDetail.jobs) { if (job.skills) { - const usersPromises = []; + const usersPromises = [] _.map(job.skills, (skillId) => { - usersPromises.push(helper.getSkillById(skillId)); - }); - jobSkills = await Promise.all(usersPromises); - job.skills = jobSkills; + usersPromises.push(helper.getSkillById(skillId)) + }) + jobSkills = await Promise.all(usersPromises) + job.skills = jobSkills } } } - return teamDetail; + return teamDetail } getTeam.schema = Joi.object() .keys({ currentUser: Joi.object().required(), - id: Joi.number().integer().required(), + id: Joi.number().integer().required() }) - .required(); + .required() /** * Get team job with id @@ -303,25 +303,25 @@ getTeam.schema = Joi.object() * @param {String} jobId the job id * @returns the team job */ -async function getTeamJob(currentUser, id, jobId) { - const project = await helper.getProjectById(currentUser, id); - const jobs = await _getJobsByProjectIds(currentUser, [project.id]); - const job = _.find(jobs, { id: jobId }); +async function getTeamJob (currentUser, id, jobId) { + const project = await helper.getProjectById(currentUser, id) + const jobs = await _getJobsByProjectIds(currentUser, [project.id]) + const job = _.find(jobs, { id: jobId }) if (!job) { throw new errors.NotFoundError( `id: ${jobId} "Job" with Team id ${id} doesn't exist` - ); + ) } const result = { id: job.id, - title: job.title, - }; + title: job.title + } if (job.skills) { result.skills = await Promise.all( _.map(job.skills, (skillId) => helper.getSkillById(skillId)) - ); + ) } // If the job has candidates, the following data for each candidate would be populated: @@ -336,12 +336,12 @@ async function getTeamJob(currentUser, id, jobId) { _.map(_.uniq(_.map(job.candidates, 'userId')), (userId) => helper.getUserById(userId, true) ) - ); - const userMap = _.groupBy(users, 'id'); + ) + const userMap = _.groupBy(users, 'id') // find photo URLs for users - const members = await helper.getMembers(_.map(users, 'handle')); - const photoURLMap = _.groupBy(members, 'handleLower'); + const members = await helper.getMembers(_.map(users, 'handle')) + const photoURLMap = _.groupBy(members, 'handleLower') result.candidates = _.map(job.candidates, (candidate) => { const candidateData = _.pick(candidate, [ @@ -349,33 +349,33 @@ async function getTeamJob(currentUser, id, jobId) { 'resume', 'userId', 'interviews', - 'id', - ]); - const userData = userMap[candidate.userId][0]; + 'id' + ]) + const userData = userMap[candidate.userId][0] // attach user data to the candidate Object.assign( candidateData, _.pick(userData, ['handle', 'firstName', 'lastName', 'skills']) - ); + ) // attach photo URL to the candidate - const handleLower = userData.handle.toLowerCase(); + const handleLower = userData.handle.toLowerCase() if (photoURLMap[handleLower]) { - candidateData.photo_url = photoURLMap[handleLower][0].photoURL; + candidateData.photo_url = photoURLMap[handleLower][0].photoURL } - return candidateData; - }); + return candidateData + }) } - return result; + return result } getTeamJob.schema = Joi.object() .keys({ currentUser: Joi.object().required(), id: Joi.number().integer().required(), - jobId: Joi.string().guid().required(), + jobId: Joi.string().guid().required() }) - .required(); + .required() /** * Send email through a particular template @@ -383,21 +383,21 @@ getTeamJob.schema = Joi.object() * @param {Object} data the email object * @returns {undefined} */ -async function sendEmail(currentUser, data) { - const template = emailTemplates[data.template]; - const dataCC = data.cc || []; - const templateCC = template.cc || []; - const dataRecipients = data.recipients || []; - const templateRecipients = template.recipients || []; +async function sendEmail (currentUser, data) { + const template = emailTemplates[data.template] + const dataCC = data.cc || [] + const templateCC = template.cc || [] + const dataRecipients = data.recipients || [] + const templateRecipients = template.recipients || [] const subjectBody = { subject: data.subject || template.subject, - body: data.body || template.body, - }; + body: data.body || template.body + } for (const key in subjectBody) { subjectBody[key] = await helper.substituteStringByObject( subjectBody[key], data.data - ); + ) } const emailData = { // override template if coming data already have the 'from' address @@ -407,9 +407,9 @@ async function sendEmail(currentUser, data) { cc: _.uniq([...dataCC, ...templateCC]), data: { ...data.data, ...subjectBody }, sendgrid_template_id: template.sendgridTemplateId, - version: 'v3', - }; - await helper.postEvent(config.EMAIL_TOPIC, emailData); + version: 'v3' + } + await helper.postEvent(config.EMAIL_TOPIC, emailData) } sendEmail.schema = Joi.object() @@ -423,11 +423,11 @@ sendEmail.schema = Joi.object() data: Joi.object().required(), from: Joi.string().email(), recipients: Joi.array().items(Joi.string().email()).allow(null), - cc: Joi.array().items(Joi.string().email()).allow(null), + cc: Joi.array().items(Joi.string().email()).allow(null) }) - .required(), + .required() }) - .required(); + .required() /** * Add a member to a team as customer. @@ -437,25 +437,25 @@ sendEmail.schema = Joi.object() * @param {String} fields the fields to be returned * @returns {Object} the member added */ -async function _addMemberToProjectAsCustomer(projectId, userId, fields) { +async function _addMemberToProjectAsCustomer (projectId, userId, fields) { try { const member = await helper.createProjectMember( projectId, { userId: userId, role: 'customer' }, { fields } - ); - return member; + ) + return member } catch (err) { - err.message = _.get(err, 'response.body.message') || err.message; + err.message = _.get(err, 'response.body.message') || err.message if (err.message && err.message.includes('User already registered')) { - throw new Error('User is already added'); + throw new Error('User is already added') } logger.error({ component: 'TeamService', context: '_addMemberToProjectAsCustomer', - message: err.message, - }); - throw err; + message: err.message + }) + throw err } } @@ -467,16 +467,16 @@ async function _addMemberToProjectAsCustomer(projectId, userId, fields) { * @param {Object} data the object including members with handle/email to be added * @returns {Object} the success/failed added members */ -async function addMembers(currentUser, id, criteria, data) { - await helper.getProjectById(currentUser, id); // check whether the user can access the project +async function addMembers (currentUser, id, criteria, data) { + await helper.getProjectById(currentUser, id) // check whether the user can access the project const result = { success: [], - failed: [], - }; + failed: [] + } - const handles = data.handles || []; - const emails = data.emails || []; + const handles = data.handles || [] + const emails = data.emails || [] const handleMembers = await helper .getMemberDetailsByHandles(handles) @@ -484,9 +484,9 @@ async function addMembers(currentUser, id, criteria, data) { _.map(members, (member) => ({ ...member, // populate members with lower-cased handle for case insensitive search - handleLowerCase: member.handle.toLowerCase(), + handleLowerCase: member.handle.toLowerCase() })) - ); + ) const emailMembers = await helper .getMemberDetailsByEmails(emails) @@ -494,20 +494,20 @@ async function addMembers(currentUser, id, criteria, data) { _.map(members, (member) => ({ ...member, // populate members with lower-cased email for case insensitive search - emailLowerCase: member.email.toLowerCase(), + emailLowerCase: member.email.toLowerCase() })) - ); + ) await Promise.all([ Promise.all( handles.map((handle) => { const memberDetails = _.find(handleMembers, { - handleLowerCase: handle.toLowerCase(), - }); + handleLowerCase: handle.toLowerCase() + }) if (!memberDetails) { - result.failed.push({ error: "User doesn't exist", handle }); - return; + result.failed.push({ error: "User doesn't exist", handle }) + return } return _addMemberToProjectAsCustomer( @@ -517,23 +517,23 @@ async function addMembers(currentUser, id, criteria, data) { ) .then((member) => { // note, that we return `handle` in the same case it was in request - result.success.push({ ...member, handle }); + result.success.push({ ...member, handle }) }) .catch((err) => { - result.failed.push({ error: err.message, handle }); - }); + result.failed.push({ error: err.message, handle }) + }) }) ), Promise.all( emails.map((email) => { const memberDetails = _.find(emailMembers, { - emailLowerCase: email.toLowerCase(), - }); + emailLowerCase: email.toLowerCase() + }) if (!memberDetails) { - result.failed.push({ error: "User doesn't exist", email }); - return; + result.failed.push({ error: "User doesn't exist", email }) + return } return _addMemberToProjectAsCustomer( @@ -543,16 +543,16 @@ async function addMembers(currentUser, id, criteria, data) { ) .then((member) => { // note, that we return `email` in the same case it was in request - result.success.push({ ...member, email }); + result.success.push({ ...member, email }) }) .catch((err) => { - result.failed.push({ error: err.message, email }); - }); + result.failed.push({ error: err.message, email }) + }) }) - ), - ]); + ) + ]) - return result; + return result } addMembers.schema = Joi.object() @@ -561,18 +561,18 @@ addMembers.schema = Joi.object() id: Joi.number().integer().required(), criteria: Joi.object() .keys({ - fields: Joi.string(), + fields: Joi.string() }) .required(), data: Joi.object() .keys({ handles: Joi.array().items(Joi.string()), - emails: Joi.array().items(Joi.string().email()), + emails: Joi.array().items(Joi.string().email()) }) .or('handles', 'emails') - .required(), + .required() }) - .required(); + .required() /** * Search members in a team. @@ -583,9 +583,9 @@ addMembers.schema = Joi.object() * @params {Object} criteria the search criteria * @returns {Object} the search result */ -async function searchMembers(currentUser, id, criteria) { - const result = await helper.listProjectMembers(currentUser, id, criteria); - return { result }; +async function searchMembers (currentUser, id, criteria) { + const result = await helper.listProjectMembers(currentUser, id, criteria) + return { result } } searchMembers.schema = Joi.object() @@ -595,11 +595,11 @@ searchMembers.schema = Joi.object() criteria: Joi.object() .keys({ role: Joi.string(), - fields: Joi.string(), + fields: Joi.string() }) - .required(), + .required() }) - .required(); + .required() /** * Search member invites for a team. @@ -610,13 +610,13 @@ searchMembers.schema = Joi.object() * @params {Object} criteria the search criteria * @returns {Object} the search result */ -async function searchInvites(currentUser, id, criteria) { +async function searchInvites (currentUser, id, criteria) { const result = await helper.listProjectMemberInvites( currentUser, id, criteria - ); - return { result }; + ) + return { result } } searchInvites.schema = Joi.object() @@ -625,11 +625,11 @@ searchInvites.schema = Joi.object() id: Joi.number().integer().required(), criteria: Joi.object() .keys({ - fields: Joi.string(), + fields: Joi.string() }) - .required(), + .required() }) - .required(); + .required() /** * Remove a member from a team. @@ -640,17 +640,17 @@ searchInvites.schema = Joi.object() * @param {String} projectMemberId the id of the project member * @returns {undefined} */ -async function deleteMember(currentUser, id, projectMemberId) { - await helper.deleteProjectMember(currentUser, id, projectMemberId); +async function deleteMember (currentUser, id, projectMemberId) { + await helper.deleteProjectMember(currentUser, id, projectMemberId) } deleteMember.schema = Joi.object() .keys({ currentUser: Joi.object().required(), id: Joi.number().integer().required(), - projectMemberId: Joi.number().integer().required(), + projectMemberId: Joi.number().integer().required() }) - .required(); + .required() /** * Return details about the current user. @@ -659,31 +659,31 @@ deleteMember.schema = Joi.object() * @params {Object} criteria the search criteria * @returns {Object} the user data for current user */ -async function getMe(currentUser) { - return helper.getUserByExternalId(currentUser.userId); +async function getMe (currentUser) { + return helper.getUserByExternalId(currentUser.userId) } getMe.schema = Joi.object() .keys({ - currentUser: Joi.object().required(), + currentUser: Joi.object().required() }) - .required(); + .required() /** * @param {Object} currentUser the user performing the operation. * @param {Object} data project data * @returns {Object} the created project */ -async function createProj(currentUser, data) { - return helper.createProject(currentUser, data); +async function createProj (currentUser, data) { + return helper.createProject(currentUser, data) } createProj.schema = Joi.object() .keys({ currentUser: Joi.object().required(), - data: Joi.object().required(), + data: Joi.object().required() }) - .required(); + .required() module.exports = { searchTeams, @@ -695,5 +695,5 @@ module.exports = { searchInvites, deleteMember, getMe, - createProj, -}; + createProj +} From 0304e683e2df5037ec0695c9faaf4b6d7fff5aa4 Mon Sep 17 00:00:00 2001 From: Cagdas U Date: Sat, 29 May 2021 13:04:56 +0300 Subject: [PATCH 20/86] feat(job-service): accept `roles` array * Add `roles` in Job schema. * Accept `roles` in the Job related endpoints. * Fix demo-data (was blocking `npm run local:init` script). --- data/demo-data.json | 12 ++++++------ src/common/helper.js | 1 + src/models/Job.js | 3 +++ src/services/JobService.js | 17 +++++++++++++---- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/data/demo-data.json b/data/demo-data.json index e0733443..8c5e8509 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -184,7 +184,7 @@ "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", "googleCalendarId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, "attendeesList": null, @@ -213,7 +213,7 @@ "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", "googleCalendarId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, "attendeesList": null, @@ -228,7 +228,7 @@ "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", "googleCalendarId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, "attendeesList": null, @@ -257,7 +257,7 @@ "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", "googleCalendarId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 3, "startTimestamp": null, "attendeesList": [ @@ -275,7 +275,7 @@ "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", "googleCalendarId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, "attendeesList": [ @@ -293,7 +293,7 @@ "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", "googleCalendarId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, "attendeesList": null, diff --git a/src/common/helper.js b/src/common/helper.js index e68e5c58..6333457e 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -111,6 +111,7 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { rateType: { type: 'keyword' }, workload: { type: 'keyword' }, skills: { type: 'keyword' }, + roles: { type: 'keyword' }, status: { type: 'keyword' }, isApplicationPageActive: { type: 'boolean' }, createdAt: { type: 'date' }, diff --git a/src/models/Job.js b/src/models/Job.js index 49d34ff7..62305000 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -94,6 +94,9 @@ module.exports = (sequelize) => { type: Sequelize.JSONB, allowNull: false }, + roles: { + type: Sequelize.ARRAY(Sequelize.UUID) + }, status: { type: Sequelize.STRING(255), allowNull: false diff --git a/src/services/JobService.js b/src/services/JobService.js index 61685901..482d0c34 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -177,6 +177,7 @@ createJob.schema = Joi.object().keys({ rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()).required(), + roles: Joi.array().items(Joi.string().uuid()).allow(null), isApplicationPageActive: Joi.boolean() }).required() }).required() @@ -245,6 +246,7 @@ partiallyUpdateJob.schema = Joi.object().keys({ rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()), + roles: Joi.array().items(Joi.string().uuid()).allow(null), isApplicationPageActive: Joi.boolean() }).required() }).required() @@ -361,6 +363,7 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } 'startDate', 'resourceType', 'skill', + 'role', 'rateType', 'workload', 'title', @@ -375,10 +378,10 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } } } } - } else if (key === 'skill') { + } else if (key === 'skill' || key === 'role') { must = { terms: { - skills: [value] + [`${key}s`]: [value] } } } else { @@ -453,9 +456,14 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } [Op.like]: `%${criteria.title}%` } } - if (criteria.skills) { + if (criteria.skill) { filter.skills = { - [Op.contains]: [criteria.skills] + [Op.contains]: [criteria.skill] + } + } + if (criteria.role) { + filter.roles = { + [Op.contains]: [criteria.role] } } if (criteria.jobIds && criteria.jobIds.length > 0) { @@ -495,6 +503,7 @@ searchJobs.schema = Joi.object().keys({ startDate: Joi.date(), resourceType: Joi.string(), skill: Joi.string().uuid(), + role: Joi.string().uuid(), rateType: Joi.rateType(), workload: Joi.workload(), status: Joi.jobStatus(), From 9732e1e179c33ee8852cd59ed14f95672afcb9c4 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Sun, 30 May 2021 23:23:27 +0300 Subject: [PATCH 21/86] fix: resource booking search issues --- data/demo-data.json | 5635 ++++++++++++++++- ...coder-bookings-api.postman_collection.json | 2056 +++++- ...topcoder-bookings.postman_environment.json | 221 +- src/common/helper.js | 1100 ++-- src/services/ResourceBookingService.js | 59 +- 5 files changed, 8040 insertions(+), 1031 deletions(-) diff --git a/data/demo-data.json b/data/demo-data.json index e0733443..57fcb97a 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -99,6 +99,497 @@ "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:12:09.293Z", "updatedAt": "2021-05-09T21:14:59.157Z" + }, + { + "id": "ff76b81d-f49b-4019-b50e-c7932a818f19", + "projectId": 17232, + "externalId": "51974177", + "description": "Welcome to the “Design Practice Challenge - Design a User Profile Screen”!\nThis challenge is created specifically for you, our new member, to learn/practice and get started with Design Competitions at Topcoder by participating and submitting to this very simple challenge where we are asking you to create a single design screen that shows a user’s profile information. You are free to create the design using design tools like Adobe XD, Figma, Sketch, or Photoshop!\nMake sure you register for the challenge and then read more details below and let us know if you have any doubts or questions in The Challenge Forum! Topcoder Design competitions provide coverage for a full range of design needs, from responsive and mobile application user experience to marketing collateral creation and support. To be specific, there are seven types of design challenges that we offer in the Design track: Application Front End Design Web Design Widget or Mobile Screen Design Wireframes Print/Presentation Design First2Finish Idea Generation\nYou must ensure your design submission addresses all the requirements mentioned in the challenge specification, and ensure your design follows the best practice for the specific interface or devices.\nTopcoder design challenges usually offer cash prizes for the three top winners and five checkpoint winners, and sometimes beyond that for special challenges like RUX (five top winners and eight rolling checkpoint winners) or LUX (eight top winners and eight rolling checkpoint winners).\nIn this practice challenge, we bring a basic Design Challenge for you so that you can adapt and get accustomed to the challenge phases.\nEach design challenge goes through the following time period phases: Registration - Duration in which you can register for the challenge 1st Round/Checkpoint Submission - Duration in which you can submit according to round one requirements Checkpoint Screening - Duration in which a Topcoder Screener will check your checkpoint design submission Checkpoint Review - Duration in which DRB and client will review submissions from round one and provide feedback 2nd Round/Final Submission - Duration in which all first-round submitters can submit design work after applying the checkpoint feedback received from DRB and Client\nAfter your final submission, the following phases take place: Final Screening, Final Review, Final Fixes, and Approval.\nThe majority of design challenges at Topcoder usually use the two-round format unless it is a one-round type challenge such as a Design First2Finish, Rapid User Experience (RUX), or Live User Experience (LUX)\nFor more help, we have a Detailed Guide about how to compete in a Design Challenge or check out This Video.\nMake sure you register for the challenge and then read more details below and let us know if you have any doubts or questions in The Challenge Forum!\nOVERVIEW Create a single UI Design Screen for any web device for a user’s profile.\nCHALLENGE REQUIREMENTS Please make sure to create and include the following requests in the User Profile screen: Name and Profile Picture such that the audience can easily recognize the user. About to give some brief information about the user. Skills and Specialization to emphasize a user’s strength. Profile links linking to user’s other important and relevant websites (Facebook, Twitter, Dribbble, Github, etc) You can check a sample of Topcoder existing user profiles to get an idea Here Feel free to use and copy content from Topcoder’s Profile Page and create your own version for the user profile screen Quotes? Hobbies? Interest? Etc? Feel free to add as many details as you want to make your design better!\nTARGET AUDIENCE Topcoder members that want to know about you and your history at Topcoder Potential clients that want to know about your skills and achievement at Topcoder before they hire you\nJUDGING CRITERIA SCORE Creativity: 8 1: barely new ideas Aesthetics: 10 1: low-fidelity design, wireframe or plain sketch Exploration: 7 1: strictly follow an existing reference or production guideline Branding: 10 1: don’t care at all about the branding just functionality\nBRANDING GUIDELINES Open to Designers (Font, Colors, Style, etc)\nTARGET DEVICES Web App: Minimum 1440px width with height adjusted accordingly\nFINAL SUBMISSION GUIDELINES All original source files created in Adobe XD, Adobe Photoshop, Figma, or Sketch Marvel Prototype: Upload your screens to Marvel App Ask for Marvel Prototype access in the challenge forum Include your Marvel App URL as a text file in your final submission. Label the file as “MarvelApp URL”", + "title": "Job Taas", + "startDate": "2021-03-08T15:00:00.000Z", + "duration": 4, + "numPositions": 2, + "resourceType": "software-developer", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "4cb4c2c3-f8af-49e4-8b67-6318d5d1d329", + "ad43a3f3-413f-4bfe-9703-28afad49b116", + "d67a5932-3f8d-4a5a-88cf-d7b706aae2d5", + "b4e1d1d2-794f-486d-8629-b9d7f26af5c6", + "0b104b7c-0792-4118-8bc7-a274e9ee19e3" + ], + "status": "closed", + "isApplicationPageActive": false, + "createdBy": "fe38eed1-af73-41fd-85a2-ac4da1ff09a3", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-03-19T08:11:26.966Z", + "updatedAt": "2021-03-23T04:35:03.686Z" + }, + { + "id": "ff753824-919c-4712-9197-49d7edaa4db7", + "projectId": 17430, + "externalId": "54439701", + "description": null, + "title": "job-Thu May 27 2021 11:58:55 GMT+0530 (India Standard Time)", + "startDate": null, + "duration": null, + "numPositions": 1, + "resourceType": null, + "rateType": null, + "workload": null, + "skills": [ + "5843b329-433c-4a17-a5b2-570caaa34baf" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "71c5e6a8-51d9-4fb5-91ce-d974642531af", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-27T06:28:58.039Z", + "updatedAt": "2021-05-27T06:29:06.204Z" + }, + { + "id": "ff3feeae-d4f7-457c-bff7-215be5efe2b8", + "projectId": 16781, + "externalId": "0", + "description": "Designer", + "title": "Designer", + "startDate": "2020-12-14T12:41:13.019Z", + "duration": null, + "numPositions": 5, + "resourceType": "desiger", + "rateType": "hourly", + "workload": "full-time", + "skills": [], + "status": "assigned", + "isApplicationPageActive": false, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2020-12-14T12:41:14.323Z", + "updatedAt": "2021-02-01T07:14:33.994Z" + }, + { + "id": "fefd2618-9b66-4431-9874-1d02d7a37d90", + "projectId": 17324, + "externalId": "53432162", + "description": "

Python Data Science

", + "title": "Data Scientist", + "startDate": "2021-07-06T18:30:00.000Z", + "duration": 9, + "numPositions": 2, + "resourceType": "data-scientist", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "2752d41b-1082-478b-9fd4-6396f510a130", + "bfc796ee-0c25-4838-ab23-5b007dee7672" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-13T08:51:26.517Z", + "updatedAt": "2021-05-13T08:52:02.354Z" + }, + { + "id": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "projectId": 17103, + "externalId": null, + "description": "# Heading\n\n**bold**\n*italic*\n~~crossed~~\n\n- - -\n\n> test\n\n* 1\n * 2\n\n1. 2\n 1. 3\n\n* [ ] asdfsdf\n\n
\n| 1 | 1 |\n| --- | --- |\n| 2 | 2 |\n\n![test](https://user-images.githubusercontent.com/146016/108334984-235ce880-71db-11eb-8643-263675197b2c.png)\n[https://user-images.githubusercontent.com/146016/108334984-235ce880-71db-11eb-8643-263675197b2c.png](https://user-images.githubusercontent.com/146016/108334984-235ce880-71db-11eb-8643-263675197b2c.png)\n`asdfsadfasdf`\n\n
\n```\nasdsdf\n```", + "title": "1", + "startDate": null, + "duration": 1, + "numPositions": 10, + "resourceType": "software-developer", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "24ba39a5-0b75-41f3-9a33-4f8063fcd828" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "077fd578-7463-457f-b6ef-22c02178d7f5", + "updatedBy": null, + "createdAt": "2021-02-21T09:26:04.620Z", + "updatedAt": "2021-02-21T09:26:04.620Z" + }, + { + "id": "fed687e1-4257-48bb-806c-38712f9bf14f", + "projectId": 16870, + "externalId": "1212", + "description": "Dummy Description", + "title": "Dummy title - at most 64 characters", + "startDate": "2020-09-27T04:17:23.131Z", + "duration": 2, + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "monthly", + "workload": "full-time", + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "077fd578-7463-457f-b6ef-22c02178d7f5", + "createdAt": "2021-02-04T10:25:20.358Z", + "updatedAt": "2021-02-18T13:48:13.326Z" + }, + { + "id": "fed14737-9ea2-4d90-b26c-781ad689b4ae", + "projectId": 16949, + "externalId": null, + "description": null, + "title": "Test", + "startDate": null, + "duration": null, + "numPositions": 1, + "resourceType": null, + "rateType": "weekly", + "workload": "full-time", + "skills": [], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", + "updatedBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", + "createdAt": "2021-01-31T20:29:34.581Z", + "updatedAt": "2021-01-31T22:00:51.803Z" + }, + { + "id": "fe8da845-5313-496f-b859-9824bd06a0db", + "projectId": 16870, + "externalId": "1212", + "description": null, + "title": "Max Dummy title - at most 64 characters", + "startDate": null, + "duration": 3, + "numPositions": 13, + "resourceType": null, + "rateType": null, + "workload": null, + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "in-review", + "isApplicationPageActive": false, + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedBy": "077fd578-7463-457f-b6ef-22c02178d7f5", + "createdAt": "2021-01-11T08:29:29.153Z", + "updatedAt": "2021-02-18T13:48:30.718Z" + }, + { + "id": "fe600350-0a6d-4dac-922f-a6a7d285daa1", + "projectId": 17290, + "externalId": "52700649", + "description": "test", + "title": "PICACHUI APR 30 JOB 5", + "startDate": "2021-04-30T18:30:00.000Z", + "duration": 5, + "numPositions": 5, + "resourceType": "software-developer", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "ad43a3f3-413f-4bfe-9703-28afad49b116" + ], + "status": "in-review", + "isApplicationPageActive": false, + "createdBy": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-04-30T07:56:22.940Z", + "updatedAt": "2021-04-30T08:06:25.212Z" + }, + { + "id": "fe539bef-9119-4a8c-b7b0-915e7e3a3ba3", + "projectId": 16870, + "externalId": "1212", + "description": "Dummy Description", + "title": "Dummy title - at most 64 characters", + "startDate": "2020-09-27T04:17:23.131Z", + "duration": null, + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedBy": null, + "createdAt": "2021-01-07T19:42:10.255Z", + "updatedAt": "2021-05-30T11:14:08.434Z" + }, + { + "id": "fe481d1c-cf87-49c1-9370-695f9f754041", + "projectId": 16762, + "externalId": "0", + "description": "Designer #1", + "title": "Designer #1", + "startDate": "2020-09-27T04:17:23.131Z", + "duration": null, + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "ee4c50c1-c8c3-475e-b6b6-edbd136a19d6", + "89139c80-d0a2-47c2-aa16-14589d5afd10", + "9f2d9127-6a2e-4506-ad76-c4ab63577b09", + "9515e7ee-83b6-49d1-ba5c-6c59c5a8ef1b", + "c854ab55-5922-4be1-8ecc-b3bc1f8629af", + "8456002e-fa2d-44f0-b0e7-86b1c02b6e4c", + "114b4ec8-805e-4c60-b351-14a955a991a9", + "213408aa-f16f-46c8-bc57-9e569cee3f11", + "b37a48db-f775-4e4e-b403-8ad1d234cdea", + "99b930b5-1b91-4df1-8b17-d9307107bb51", + "6388a632-c3ad-4525-9a73-66a527c03672", + "23839f38-6f19-4de9-9d28-f020056bca73", + "289e42a3-23e9-49be-88e1-6deb93cd8c31", + "b403f209-63b5-42bc-9b5f-1564416640d8" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedBy": null, + "createdAt": "2020-12-08T15:56:18.707Z", + "updatedAt": "2021-05-30T11:14:08.434Z" + }, + { + "id": "fe270791-bc24-4f6a-8c1b-b897f5d97d2f", + "projectId": 16899, + "externalId": null, + "description": "Software Developer Full time", + "title": "Software Developer", + "startDate": null, + "duration": null, + "numPositions": 1, + "resourceType": "software-developer", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "e2b8acc2-881f-45a6-8321-489976b1db21", + "4fce6ced-3610-443c-92eb-3f6d76b34f5c", + "5c6c79ee-a8fe-4ec2-b9df-813bd67df413", + "24ba39a5-0b75-41f3-9a33-4f8063fcd828", + "4cb4c2c3-f8af-49e4-8b67-6318d5d1d329", + "1fd02aad-e08a-4669-9ffd-181468fea694" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": null, + "createdAt": "2021-01-12T08:57:01.987Z", + "updatedAt": "2021-05-30T11:14:08.434Z" + }, + { + "id": "fd48d96e-b0f2-43b7-8a48-f4fa194d6bc8", + "projectId": 17363, + "externalId": "54085472", + "description": "Test", + "title": "Test Job May 25", + "startDate": null, + "duration": 2, + "numPositions": 5, + "resourceType": "designer", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "24ba39a5-0b75-41f3-9a33-4f8063fcd828" + ], + "status": "in-review", + "isApplicationPageActive": false, + "createdBy": "fe38eed1-af73-41fd-85a2-ac4da1ff09a3", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-25T11:03:00.164Z", + "updatedAt": "2021-05-27T04:48:06.194Z" + }, + { + "id": "fd13ad99-f16a-4362-9274-80f5f38895c3", + "projectId": 17300, + "externalId": "52885626", + "description": "Description", + "title": "Test Job RB 3", + "startDate": null, + "duration": 1, + "numPositions": 3, + "resourceType": "designer", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "4fce6ced-3610-443c-92eb-3f6d76b34f5c" + ], + "status": "in-review", + "isApplicationPageActive": false, + "createdBy": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-01T09:43:13.357Z", + "updatedAt": "2021-05-01T09:44:53.411Z" + }, + { + "id": "fc5ba131-566f-46fe-8501-79c593241896", + "projectId": 16739, + "externalId": "0", + "description": "Designer", + "title": "Designer", + "startDate": "2020-12-07T09:30:37.304Z", + "duration": null, + "numPositions": 12, + "resourceType": "desiger", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "ee4c50c1-c8c3-475e-b6b6-edbd136a19d6", + "89139c80-d0a2-47c2-aa16-14589d5afd10", + "c854ab55-5922-4be1-8ecc-b3bc1f8629af", + "38471151-8513-4e04-b0fe-80331556abd9", + "8456002e-fa2d-44f0-b0e7-86b1c02b6e4c", + "9515e7ee-83b6-49d1-ba5c-6c59c5a8ef1b", + "b37a48db-f775-4e4e-b403-8ad1d234cdea", + "9f2d9127-6a2e-4506-ad76-c4ab63577b09", + "213408aa-f16f-46c8-bc57-9e569cee3f11", + "114b4ec8-805e-4c60-b351-14a955a991a9", + "afc39636-f7d7-4349-aef5-f9a76cc9e155", + "c2ac5154-6a0d-425d-9c90-9fd0af5d88a5", + "bfc796ee-0c25-4838-ab23-5b007dee7672", + "fbf792fc-0920-4b54-866a-f48c386994a0", + "63189fe0-94a9-4e2e-90f3-ef764e6a005b", + "87a8dd0f-12ea-47cd-a80d-20f47f0b7b9a", + "7a359e66-82bd-429c-8f95-8913bf00e67b" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedBy": null, + "createdAt": "2020-12-07T09:30:37.470Z", + "updatedAt": "2021-05-30T11:14:08.434Z" + }, + { + "id": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "projectId": 16805, + "externalId": "1212", + "description": "QA Demo Description", + "title": "QA Demo Description", + "startDate": "2020-09-27T04:17:23.131Z", + "duration": null, + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "cancelled", + "isApplicationPageActive": false, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2020-12-24T06:48:56.943Z", + "updatedAt": "2020-12-24T06:49:28.997Z" + }, + { + "id": "fc2b006d-997b-49c3-a414-59ee54a48f9f", + "projectId": 16706, + "externalId": "10003", + "description": "Dummy10003 Description", + "title": "Dummy10003 Description", + "startDate": "2020-11-28T04:17:23.131Z", + "duration": null, + "numPositions": 9, + "resourceType": "Dummy Resource Type", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "cb01fd31-e8d2-4e34-8bf3-b149705de3e1" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedBy": null, + "createdAt": "2020-11-20T05:30:03.014Z", + "updatedAt": "2021-05-30T11:14:08.434Z" + }, + { + "id": "fc0240f0-8c8f-40ce-a551-e83b45673098", + "projectId": 16714, + "externalId": "2227", + "description": "zapier2227 Description", + "title": "zapier2227 Description", + "startDate": "2021-01-04T04:17:23.131Z", + "duration": null, + "numPositions": 8, + "resourceType": "Dummy2227 Resource Type", + "rateType": "weekly", + "workload": "full-time", + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedBy": null, + "createdAt": "2020-12-08T13:50:26.805Z", + "updatedAt": "2021-05-30T11:14:08.434Z" + }, + { + "id": "fb8b92f6-4ffb-4ba6-8c38-c2d4a151f76b", + "projectId": 17091, + "externalId": "12312", + "description": "second Job created from api", + "title": "second Job from api", + "startDate": "2021-02-27T04:17:23.131Z", + "duration": null, + "numPositions": 12, + "resourceType": "software Developer", + "rateType": "hourly", + "workload": "full-time", + "skills": [ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "71c5e6a8-51d9-4fb5-91ce-d974642531af", + "updatedBy": null, + "createdAt": "2021-02-18T09:43:11.985Z", + "updatedAt": "2021-02-18T09:43:11.985Z" + }, + { + "id": "fb2f5f9b-5874-4dcd-af94-727fc0409760", + "projectId": 16718, + "externalId": "12131", + "description": "# Awesome Editor!\n\nIt has been *released as opensource in 2018* and has\n~~continually~~\n evolved to **receive 10k GitHub ⭐️ Stars**.\n\n## Create Instance\n\nYou can create an instance with the following code and use `getHtml()` and `getMarkdown()` of the [Editor](https://github.com/nhn/tui.editor).\n
\n``` js\nconst editor = new Editor(options);\n```\n\n> See the table below for default options\n> \n> \n> \n> > More API information can be found in the document\n\n| name | type | description |\n| ---- | ---- | ----------- |\n| el | `HTMLElement` | container element |\n\n## Features\n\n* CommonMark + GFM Specifications\n * Live Preview\n * Scroll Sync\n * Auto Indent\n * Syntax Highlight\n 1. Markdown\n 2. Preview\n\n## Support Wrappers\n\n> * Wrappers\n> \n> 1. [x] React\n> 2. [x] Vue\n> 3. [ ] Ember", + "title": "aaaaa", + "startDate": "2021-02-27T04:17:23.131Z", + "duration": null, + "numPositions": 3, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "workload": "full-time", + "skills": [], + "status": "sourcing", + "isApplicationPageActive": false, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "077fd578-7463-457f-b6ef-22c02178d7f5", + "createdAt": "2021-01-25T10:07:56.847Z", + "updatedAt": "2021-02-23T13:02:59.708Z" } ], "JobCandidate": [ @@ -181,18 +672,29 @@ "interviews": [ { "id": "077aa2ca-5b60-4ad9-a965-1b37e08a5046", + "xaiId": null, "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", - "googleCalendarId": null, - "customMessage": null, - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 1, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Completed", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:16:10.887Z", - "updatedAt": "2021-05-09T21:16:10.887Z" + "updatedAt": "2021-05-09T21:16:10.887Z", + "deletedAt": null } ] }, @@ -210,33 +712,55 @@ "interviews": [ { "id": "b1f7ba76-640f-47e2-9463-59e51b51ec60", + "xaiId": null, "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 2, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Scheduling", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:17:23.517Z", - "updatedAt": "2021-05-09T21:17:23.517Z" + "updatedAt": "2021-05-09T21:17:23.517Z", + "deletedAt": null }, { "id": "3144fa65-ea1a-4bec-81b0-7cb1c8845826", + "xaiId": null, "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": null, - "customMessage": null, - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 1, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Completed", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:16:39.019Z", - "updatedAt": "2021-05-09T21:16:39.019Z" + "updatedAt": "2021-05-09T21:16:39.019Z", + "deletedAt": null } ] }, @@ -254,54 +778,81 @@ "interviews": [ { "id": "976d23a9-5710-453f-99d9-f57a588bb610", + "xaiId": null, "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 3, "startTimestamp": null, - "attendeesList": [ - "attendee1@yopmail.com", - "attendee2@yopmail.com" - ], + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Scheduling", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:21:28.713Z", - "updatedAt": "2021-05-09T21:21:28.713Z" + "updatedAt": "2021-05-09T21:21:28.713Z", + "deletedAt": null }, { "id": "a23e1bf2-1084-4cfe-a0d8-d83bc6fec655", + "xaiId": null, "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 2, "startTimestamp": null, - "attendeesList": [ - "attendee1@yopmail.com", - "attendee2@yopmail.com" - ], + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Scheduling", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:21:22.428Z", - "updatedAt": "2021-05-09T21:21:22.428Z" + "updatedAt": "2021-05-09T21:21:22.428Z", + "deletedAt": null }, { "id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb", + "xaiId": null, "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": null, - "customMessage": null, - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 1, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Completed", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:21:17.346Z", - "updatedAt": "2021-05-09T21:21:17.346Z" + "updatedAt": "2021-05-09T21:21:17.346Z", + "deletedAt": null } ] }, @@ -640,16 +1191,16 @@ "updatedAt": "2021-05-09T21:45:32.659Z", "payments": [ { - "id": "03a0163c-472e-4ea6-b8ad-3dc86d418ecf", + "id": "1c682ea9-ba63-4fcc-b00c-049d2458d3ac", "workPeriodId": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 210.19, - "status": "cancelled", + "amount": 57.79, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:36.932Z", - "updatedAt": "2021-05-09T21:31:36.932Z" + "createdAt": "2021-05-09T21:31:35.726Z", + "updatedAt": "2021-05-09T21:31:35.726Z" }, { "id": "14b266c6-e76a-4042-b439-74fe3e42c90f", @@ -664,16 +1215,16 @@ "updatedAt": "2021-05-09T21:31:38.183Z" }, { - "id": "1c682ea9-ba63-4fcc-b00c-049d2458d3ac", + "id": "03a0163c-472e-4ea6-b8ad-3dc86d418ecf", "workPeriodId": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 57.79, - "status": "completed", + "amount": 210.19, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:35.726Z", - "updatedAt": "2021-05-09T21:31:35.726Z" + "createdAt": "2021-05-09T21:31:36.932Z", + "updatedAt": "2021-05-09T21:31:36.932Z" } ] }, @@ -694,28 +1245,28 @@ "updatedAt": "2021-05-09T21:45:37.647Z", "payments": [ { - "id": "e8f3d379-f5a0-47f6-b37b-cae24f5909e9", + "id": "cc235aee-0911-4869-bb49-911507bb31e7", "workPeriodId": "e8346d7b-4ada-428d-a768-c2989306f63a", "challengeId": "00000000-0000-0000-0000-000000000000", "amount": 494.46, - "status": "completed", + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:22.403Z", - "updatedAt": "2021-05-09T21:34:22.403Z" + "createdAt": "2021-05-09T21:34:26.807Z", + "updatedAt": "2021-05-09T21:34:26.807Z" }, { - "id": "cc235aee-0911-4869-bb49-911507bb31e7", + "id": "e8f3d379-f5a0-47f6-b37b-cae24f5909e9", "workPeriodId": "e8346d7b-4ada-428d-a768-c2989306f63a", "challengeId": "00000000-0000-0000-0000-000000000000", "amount": 494.46, - "status": "cancelled", + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:26.807Z", - "updatedAt": "2021-05-09T21:34:26.807Z" + "createdAt": "2021-05-09T21:34:22.403Z", + "updatedAt": "2021-05-09T21:34:22.403Z" } ] }, @@ -796,28 +1347,28 @@ "updatedAt": "2021-05-09T21:45:27.504Z", "payments": [ { - "id": "fcd10a26-3548-4f9b-9e2b-20397d057800", + "id": "40e862a0-8772-4587-88b4-23acff8eb2e0", "workPeriodId": "61c1e7e3-5e0a-4892-9099-872bc4c11a22", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 494.46, + "amount": 417.42, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:41.381Z", - "updatedAt": "2021-05-09T21:32:41.381Z" + "createdAt": "2021-05-09T21:32:40.091Z", + "updatedAt": "2021-05-09T21:32:40.091Z" }, { - "id": "40e862a0-8772-4587-88b4-23acff8eb2e0", + "id": "fcd10a26-3548-4f9b-9e2b-20397d057800", "workPeriodId": "61c1e7e3-5e0a-4892-9099-872bc4c11a22", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 417.42, + "amount": 494.46, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:40.091Z", - "updatedAt": "2021-05-09T21:32:40.091Z" + "createdAt": "2021-05-09T21:32:41.381Z", + "updatedAt": "2021-05-09T21:32:41.381Z" } ] } @@ -857,28 +1408,28 @@ "updatedAt": "2021-05-09T21:46:59.354Z", "payments": [ { - "id": "c8be508d-2eb5-4712-8bd7-1b28e870abc2", + "id": "9785ae89-05dc-4bcc-a030-52bd0e681d41", "workPeriodId": "b0758857-0221-47a5-a444-e263e5d9e1cf", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, - "status": "completed", + "amount": 168.54, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:02.733Z", - "updatedAt": "2021-05-09T21:34:02.733Z" + "createdAt": "2021-05-09T21:34:01.410Z", + "updatedAt": "2021-05-09T21:34:01.410Z" }, { - "id": "9785ae89-05dc-4bcc-a030-52bd0e681d41", + "id": "c8be508d-2eb5-4712-8bd7-1b28e870abc2", "workPeriodId": "b0758857-0221-47a5-a444-e263e5d9e1cf", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 168.54, - "status": "cancelled", + "amount": 448.51, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:01.410Z", - "updatedAt": "2021-05-09T21:34:01.410Z" + "createdAt": "2021-05-09T21:34:02.733Z", + "updatedAt": "2021-05-09T21:34:02.733Z" } ] }, @@ -899,28 +1450,28 @@ "updatedAt": "2021-05-09T21:47:05.373Z", "payments": [ { - "id": "c3b71f96-7680-459b-82b2-f6eb3c3f6c8f", + "id": "3ed31706-0e99-4084-81f4-b126a1a68db6", "workPeriodId": "176db0d0-474f-4590-831a-547d596c01b4", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 119.32, + "amount": 144.16, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:18.342Z", - "updatedAt": "2021-05-09T21:31:18.342Z" + "createdAt": "2021-05-09T21:31:17.193Z", + "updatedAt": "2021-05-09T21:31:17.193Z" }, { - "id": "3ed31706-0e99-4084-81f4-b126a1a68db6", + "id": "c3b71f96-7680-459b-82b2-f6eb3c3f6c8f", "workPeriodId": "176db0d0-474f-4590-831a-547d596c01b4", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 144.16, + "amount": 119.32, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:17.193Z", - "updatedAt": "2021-05-09T21:31:17.193Z" + "createdAt": "2021-05-09T21:31:18.342Z", + "updatedAt": "2021-05-09T21:31:18.342Z" } ] }, @@ -941,28 +1492,28 @@ "updatedAt": "2021-05-09T21:47:12.015Z", "payments": [ { - "id": "69445cbf-6d94-49a5-b2aa-65459ec78594", + "id": "663f11df-7832-431a-a46e-ad8c890ae52b", "workPeriodId": "5a174833-cb08-49f5-b077-cffb8e60ca01", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 417.42, - "status": "completed", + "amount": 55.6, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:33.664Z", - "updatedAt": "2021-05-09T21:32:33.664Z" + "createdAt": "2021-05-09T21:32:32.606Z", + "updatedAt": "2021-05-09T21:32:32.606Z" }, { - "id": "663f11df-7832-431a-a46e-ad8c890ae52b", + "id": "69445cbf-6d94-49a5-b2aa-65459ec78594", "workPeriodId": "5a174833-cb08-49f5-b077-cffb8e60ca01", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 55.6, - "status": "cancelled", + "amount": 417.42, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:32.606Z", - "updatedAt": "2021-05-09T21:32:32.606Z" + "createdAt": "2021-05-09T21:32:33.664Z", + "updatedAt": "2021-05-09T21:32:33.664Z" } ] }, @@ -983,28 +1534,28 @@ "updatedAt": "2021-05-09T21:47:25.687Z", "payments": [ { - "id": "8eb8fc37-5ab0-4480-8806-3d3c57ab38e1", + "id": "f65930b7-d61d-4923-bdab-54848661f151", "workPeriodId": "8c9db4fd-78ad-4e59-acba-462487b74c3a", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 496.54, + "amount": 57.79, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:07.419Z", - "updatedAt": "2021-05-09T21:33:07.419Z" + "createdAt": "2021-05-09T21:33:06.108Z", + "updatedAt": "2021-05-09T21:33:06.108Z" }, { - "id": "f65930b7-d61d-4923-bdab-54848661f151", + "id": "8eb8fc37-5ab0-4480-8806-3d3c57ab38e1", "workPeriodId": "8c9db4fd-78ad-4e59-acba-462487b74c3a", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 57.79, + "amount": 496.54, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:06.108Z", - "updatedAt": "2021-05-09T21:33:06.108Z" + "createdAt": "2021-05-09T21:33:07.419Z", + "updatedAt": "2021-05-09T21:33:07.419Z" } ] }, @@ -1025,28 +1576,28 @@ "updatedAt": "2021-05-09T21:47:30.586Z", "payments": [ { - "id": "c456755e-0432-4656-848b-64f9c5dc8f25", + "id": "dd8f5c08-d6a1-4fd2-b6bd-85fb6425d13d", "workPeriodId": "18881107-cc17-4087-9b2b-a74f04187f73", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 32.92, - "status": "cancelled", + "amount": 372.18, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:27.102Z", - "updatedAt": "2021-05-09T21:31:27.102Z" + "createdAt": "2021-05-09T21:31:25.690Z", + "updatedAt": "2021-05-09T21:31:25.690Z" }, { - "id": "dd8f5c08-d6a1-4fd2-b6bd-85fb6425d13d", + "id": "c456755e-0432-4656-848b-64f9c5dc8f25", "workPeriodId": "18881107-cc17-4087-9b2b-a74f04187f73", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 372.18, - "status": "completed", + "amount": 32.92, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:25.690Z", - "updatedAt": "2021-05-09T21:31:25.690Z" + "createdAt": "2021-05-09T21:31:27.102Z", + "updatedAt": "2021-05-09T21:31:27.102Z" } ] }, @@ -1067,28 +1618,28 @@ "updatedAt": "2021-05-09T21:47:19.034Z", "payments": [ { - "id": "10584b23-5ab2-44e2-a927-e020c08e4f84", + "id": "6d59a499-44e3-41e2-8368-5baee86dd8ab", "workPeriodId": "9b455e21-e186-4622-923a-f115d23549d1", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 293.79, + "amount": 57.79, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:29.348Z", - "updatedAt": "2021-05-09T21:33:29.348Z" + "createdAt": "2021-05-09T21:33:28.108Z", + "updatedAt": "2021-05-09T21:33:28.108Z" }, { - "id": "6d59a499-44e3-41e2-8368-5baee86dd8ab", + "id": "10584b23-5ab2-44e2-a927-e020c08e4f84", "workPeriodId": "9b455e21-e186-4622-923a-f115d23549d1", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 57.79, + "amount": 293.79, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:28.108Z", - "updatedAt": "2021-05-09T21:33:28.108Z" + "createdAt": "2021-05-09T21:33:29.348Z", + "updatedAt": "2021-05-09T21:33:29.348Z" } ] } @@ -1128,28 +1679,28 @@ "updatedAt": "2021-05-09T21:46:12.086Z", "payments": [ { - "id": "00640d2d-8330-445a-b022-aa687033b2b3", + "id": "05d09e2b-02a0-4d33-b6db-0f69a98154c6", "workPeriodId": "ac0ae325-8d77-4a73-bd85-5361165801cd", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 275.73, + "amount": 374.34, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:54.315Z", - "updatedAt": "2021-05-09T21:33:54.315Z" + "createdAt": "2021-05-09T21:33:53.131Z", + "updatedAt": "2021-05-09T21:33:53.131Z" }, { - "id": "05d09e2b-02a0-4d33-b6db-0f69a98154c6", + "id": "00640d2d-8330-445a-b022-aa687033b2b3", "workPeriodId": "ac0ae325-8d77-4a73-bd85-5361165801cd", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 374.34, + "amount": 275.73, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:53.131Z", - "updatedAt": "2021-05-09T21:33:53.131Z" + "createdAt": "2021-05-09T21:33:54.315Z", + "updatedAt": "2021-05-09T21:33:54.315Z" } ] }, @@ -1230,28 +1781,28 @@ "updatedAt": "2021-05-09T21:46:02.038Z", "payments": [ { - "id": "be9706ac-c6cb-4fff-894b-8719bcf634dc", + "id": "c7013bf0-17b5-4b15-826b-385fad41caf4", "workPeriodId": "94dde794-b730-4e05-8ea6-dcc5b541d43e", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 275.73, + "amount": 144.16, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:21.933Z", - "updatedAt": "2021-05-09T21:33:21.933Z" + "createdAt": "2021-05-09T21:33:20.680Z", + "updatedAt": "2021-05-09T21:33:20.680Z" }, { - "id": "c7013bf0-17b5-4b15-826b-385fad41caf4", + "id": "be9706ac-c6cb-4fff-894b-8719bcf634dc", "workPeriodId": "94dde794-b730-4e05-8ea6-dcc5b541d43e", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 144.16, + "amount": 275.73, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:20.680Z", - "updatedAt": "2021-05-09T21:33:20.680Z" + "createdAt": "2021-05-09T21:33:21.933Z", + "updatedAt": "2021-05-09T21:33:21.933Z" } ] }, @@ -1272,52 +1823,52 @@ "updatedAt": "2021-05-09T21:45:53.796Z", "payments": [ { - "id": "fc577d14-78e8-404c-a17b-ab496e4041d8", + "id": "6e1a114f-bfac-4ab8-93d6-e47206200540", "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 203.74, + "amount": 494.46, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:41.520Z", - "updatedAt": "2021-05-09T21:34:41.520Z" + "createdAt": "2021-05-09T21:34:40.036Z", + "updatedAt": "2021-05-09T21:34:40.036Z" }, { - "id": "fbc2d96f-f6c6-4a4d-b737-14a3564b7f70", + "id": "10fd3b3e-f5b2-42cc-91d4-54c73c003aae", "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, - "status": "cancelled", + "amount": 477.97, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:38.700Z", - "updatedAt": "2021-05-09T21:34:38.700Z" + "createdAt": "2021-05-09T21:34:37.374Z", + "updatedAt": "2021-05-09T21:34:37.374Z" }, { - "id": "10fd3b3e-f5b2-42cc-91d4-54c73c003aae", + "id": "fbc2d96f-f6c6-4a4d-b737-14a3564b7f70", "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 477.97, - "status": "completed", + "amount": 448.51, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:37.374Z", - "updatedAt": "2021-05-09T21:34:37.374Z" + "createdAt": "2021-05-09T21:34:38.700Z", + "updatedAt": "2021-05-09T21:34:38.700Z" }, { - "id": "6e1a114f-bfac-4ab8-93d6-e47206200540", + "id": "fc577d14-78e8-404c-a17b-ab496e4041d8", "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 494.46, + "amount": 203.74, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:40.036Z", - "updatedAt": "2021-05-09T21:34:40.036Z" + "createdAt": "2021-05-09T21:34:41.520Z", + "updatedAt": "2021-05-09T21:34:41.520Z" } ] }, @@ -1387,28 +1938,28 @@ "updatedAt": "2021-05-09T21:48:08.381Z", "payments": [ { - "id": "fa9bd31c-6c83-4ee4-9d45-a833cfe821f5", + "id": "abb79afc-a370-4625-a067-a3b57c9b4700", "workPeriodId": "d111a56f-593d-452e-9787-551bea504c92", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 271.42, + "amount": 448.51, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:11.662Z", - "updatedAt": "2021-05-09T21:34:11.662Z" + "createdAt": "2021-05-09T21:34:10.371Z", + "updatedAt": "2021-05-09T21:34:10.371Z" }, { - "id": "abb79afc-a370-4625-a067-a3b57c9b4700", + "id": "fa9bd31c-6c83-4ee4-9d45-a833cfe821f5", "workPeriodId": "d111a56f-593d-452e-9787-551bea504c92", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, + "amount": 271.42, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:10.371Z", - "updatedAt": "2021-05-09T21:34:10.371Z" + "createdAt": "2021-05-09T21:34:11.662Z", + "updatedAt": "2021-05-09T21:34:11.662Z" } ] }, @@ -1429,28 +1980,28 @@ "updatedAt": "2021-05-09T21:47:38.022Z", "payments": [ { - "id": "c658d66e-86e1-49c7-8051-2b9a017935ad", + "id": "3770680c-8045-43d4-8baf-cb7b3b714d39", "workPeriodId": "061f31fb-4f8c-462f-92c2-e5d275c45fde", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 248.38, - "status": "completed", + "amount": 477.97, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:41.304Z", - "updatedAt": "2021-05-09T21:30:41.304Z" + "createdAt": "2021-05-09T21:30:42.711Z", + "updatedAt": "2021-05-09T21:30:42.711Z" }, { - "id": "3770680c-8045-43d4-8baf-cb7b3b714d39", + "id": "c658d66e-86e1-49c7-8051-2b9a017935ad", "workPeriodId": "061f31fb-4f8c-462f-92c2-e5d275c45fde", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 477.97, - "status": "cancelled", + "amount": 248.38, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:42.711Z", - "updatedAt": "2021-05-09T21:30:42.711Z" + "createdAt": "2021-05-09T21:30:41.304Z", + "updatedAt": "2021-05-09T21:30:41.304Z" } ] }, @@ -1471,28 +2022,28 @@ "updatedAt": "2021-05-09T21:47:44.291Z", "payments": [ { - "id": "be4d5099-8b8e-45e2-b6cd-ab1997f57e26", + "id": "dfc9bed6-78f2-407e-a7e4-abea9a3d3b46", "workPeriodId": "5904b1d9-cb50-4b5d-8103-6741fec2f86b", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 477.97, - "status": "completed", + "amount": 48.51, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:26.174Z", - "updatedAt": "2021-05-09T21:32:26.174Z" + "createdAt": "2021-05-09T21:32:27.398Z", + "updatedAt": "2021-05-09T21:32:27.398Z" }, { - "id": "dfc9bed6-78f2-407e-a7e4-abea9a3d3b46", + "id": "be4d5099-8b8e-45e2-b6cd-ab1997f57e26", "workPeriodId": "5904b1d9-cb50-4b5d-8103-6741fec2f86b", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 48.51, - "status": "cancelled", + "amount": 477.97, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:27.398Z", - "updatedAt": "2021-05-09T21:32:27.398Z" + "createdAt": "2021-05-09T21:32:26.174Z", + "updatedAt": "2021-05-09T21:32:26.174Z" } ] }, @@ -1513,28 +2064,28 @@ "updatedAt": "2021-05-09T21:47:58.216Z", "payments": [ { - "id": "3bf29da2-581c-4f9c-8b0f-ff3c876848a0", + "id": "ea3bdc7a-7c14-4ac1-955d-2540589fcfa6", "workPeriodId": "6ed56eb5-cadc-45f8-bbdf-3f408948c274", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 293.79, + "amount": 448.51, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:59.500Z", - "updatedAt": "2021-05-09T21:32:59.500Z" + "createdAt": "2021-05-09T21:33:00.899Z", + "updatedAt": "2021-05-09T21:33:00.899Z" }, { - "id": "ea3bdc7a-7c14-4ac1-955d-2540589fcfa6", + "id": "3bf29da2-581c-4f9c-8b0f-ff3c876848a0", "workPeriodId": "6ed56eb5-cadc-45f8-bbdf-3f408948c274", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, + "amount": 293.79, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:00.899Z", - "updatedAt": "2021-05-09T21:33:00.899Z" + "createdAt": "2021-05-09T21:32:59.500Z", + "updatedAt": "2021-05-09T21:32:59.500Z" } ] }, @@ -1585,28 +2136,28 @@ "updatedAt": "2021-05-09T21:48:03.740Z", "payments": [ { - "id": "5bdd5c22-d9b7-428c-b084-d0950a18bc37", + "id": "78641310-ad51-40a3-a0fd-fdd8d15455b9", "workPeriodId": "0c825dec-6e7b-4dde-943a-f3f8354219cc", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 57.79, + "amount": 417.42, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:49.123Z", - "updatedAt": "2021-05-09T21:30:49.123Z" + "createdAt": "2021-05-09T21:30:50.460Z", + "updatedAt": "2021-05-09T21:30:50.460Z" }, { - "id": "78641310-ad51-40a3-a0fd-fdd8d15455b9", + "id": "5bdd5c22-d9b7-428c-b084-d0950a18bc37", "workPeriodId": "0c825dec-6e7b-4dde-943a-f3f8354219cc", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 417.42, + "amount": 57.79, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:50.460Z", - "updatedAt": "2021-05-09T21:30:50.460Z" + "createdAt": "2021-05-09T21:30:49.123Z", + "updatedAt": "2021-05-09T21:30:49.123Z" } ] } @@ -1646,28 +2197,28 @@ "updatedAt": "2021-05-09T21:45:01.792Z", "payments": [ { - "id": "381af41e-6b0a-49fe-987f-1bcb03fda571", + "id": "1d2b92e8-194f-477a-97f8-e104056e6b10", "workPeriodId": "13f22f72-9240-43bb-ba0c-618d0b24ad8c", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 460.88, + "amount": 39.66, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:08.119Z", - "updatedAt": "2021-05-09T21:31:08.119Z" + "createdAt": "2021-05-09T21:31:09.459Z", + "updatedAt": "2021-05-09T21:31:09.459Z" }, { - "id": "1d2b92e8-194f-477a-97f8-e104056e6b10", + "id": "381af41e-6b0a-49fe-987f-1bcb03fda571", "workPeriodId": "13f22f72-9240-43bb-ba0c-618d0b24ad8c", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 39.66, + "amount": 460.88, "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:09.459Z", - "updatedAt": "2021-05-09T21:31:09.459Z" + "createdAt": "2021-05-09T21:31:08.119Z", + "updatedAt": "2021-05-09T21:31:08.119Z" } ] }, @@ -1688,28 +2239,28 @@ "updatedAt": "2021-05-09T21:44:51.852Z", "payments": [ { - "id": "ef951baa-a007-48db-a658-495c6eeda9bc", + "id": "f0f85e56-6bf4-4e1f-a1ef-c31529efe4cd", "workPeriodId": "0cf74043-b432-41a5-99d9-83420a6ad8ef", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, - "status": "completed", + "amount": 466.42, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:59.323Z", - "updatedAt": "2021-05-09T21:30:59.323Z" + "createdAt": "2021-05-09T21:30:58.017Z", + "updatedAt": "2021-05-09T21:30:58.017Z" }, { - "id": "f0f85e56-6bf4-4e1f-a1ef-c31529efe4cd", + "id": "ef951baa-a007-48db-a658-495c6eeda9bc", "workPeriodId": "0cf74043-b432-41a5-99d9-83420a6ad8ef", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 466.42, - "status": "cancelled", + "amount": 448.51, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:58.017Z", - "updatedAt": "2021-05-09T21:30:58.017Z" + "createdAt": "2021-05-09T21:30:59.323Z", + "updatedAt": "2021-05-09T21:30:59.323Z" } ] }, @@ -1730,28 +2281,28 @@ "updatedAt": "2021-05-09T21:45:16.298Z", "payments": [ { - "id": "1cb5129e-8d92-4280-a946-8cb0f5757abc", + "id": "b576f845-7dea-4b56-b0de-6ce15fd2c245", "workPeriodId": "028287bf-6999-4fef-bdfa-1229b4e23ac1", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 460.88, + "amount": 477.97, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:33.549Z", - "updatedAt": "2021-05-09T21:30:33.549Z" + "createdAt": "2021-05-09T21:30:25.688Z", + "updatedAt": "2021-05-09T21:30:25.688Z" }, { - "id": "b576f845-7dea-4b56-b0de-6ce15fd2c245", + "id": "1cb5129e-8d92-4280-a946-8cb0f5757abc", "workPeriodId": "028287bf-6999-4fef-bdfa-1229b4e23ac1", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 477.97, + "amount": 460.88, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:25.688Z", - "updatedAt": "2021-05-09T21:30:25.688Z" + "createdAt": "2021-05-09T21:30:33.549Z", + "updatedAt": "2021-05-09T21:30:33.549Z" } ] }, @@ -1802,28 +2353,28 @@ "updatedAt": "2021-05-09T21:45:08.968Z", "payments": [ { - "id": "fa4c7e24-470f-4aef-a269-59b7e0b2bc05", + "id": "99e4bffb-90f8-411e-9a49-fc7779bd2c07", "workPeriodId": "32b977c9-386a-4159-a1c3-08169ee12f6e", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 460.88, - "status": "cancelled", + "amount": 416.38, + "status": "completed", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:56.152Z", - "updatedAt": "2021-05-09T21:31:56.152Z" + "createdAt": "2021-05-09T21:31:57.470Z", + "updatedAt": "2021-05-09T21:31:57.470Z" }, { - "id": "99e4bffb-90f8-411e-9a49-fc7779bd2c07", + "id": "fa4c7e24-470f-4aef-a269-59b7e0b2bc05", "workPeriodId": "32b977c9-386a-4159-a1c3-08169ee12f6e", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 416.38, - "status": "completed", + "amount": 460.88, + "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:57.470Z", - "updatedAt": "2021-05-09T21:31:57.470Z" + "createdAt": "2021-05-09T21:31:56.152Z", + "updatedAt": "2021-05-09T21:31:56.152Z" } ] } @@ -1893,7 +2444,7 @@ "updatedAt": "2021-05-09T21:46:39.040Z", "payments": [ { - "id": "d0ebcc96-70f2-4716-92d4-74e40af04387", + "id": "71b3b7d4-129c-4348-9ead-6f22eafa6db8", "workPeriodId": "355c7114-753a-4f99-b026-1d1430bf5530", "challengeId": "00000000-0000-0000-0000-000000000000", "amount": 416.38, @@ -1901,11 +2452,11 @@ "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:04.463Z", - "updatedAt": "2021-05-09T21:32:04.463Z" + "createdAt": "2021-05-09T21:32:05.827Z", + "updatedAt": "2021-05-09T21:32:05.827Z" }, { - "id": "71b3b7d4-129c-4348-9ead-6f22eafa6db8", + "id": "d0ebcc96-70f2-4716-92d4-74e40af04387", "workPeriodId": "355c7114-753a-4f99-b026-1d1430bf5530", "challengeId": "00000000-0000-0000-0000-000000000000", "amount": 416.38, @@ -1913,8 +2464,8 @@ "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:05.827Z", - "updatedAt": "2021-05-09T21:32:05.827Z" + "createdAt": "2021-05-09T21:32:04.463Z", + "updatedAt": "2021-05-09T21:32:04.463Z" } ] }, @@ -1995,28 +2546,28 @@ "updatedAt": "2021-05-09T21:46:44.896Z", "payments": [ { - "id": "d36afd1d-1f76-4f2d-b630-69bf85796496", + "id": "71750282-0ffe-46ed-b8c2-37e36c148833", "workPeriodId": "91fcf91f-b2cf-4909-8f03-b5efc0732b28", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 494.46, + "amount": 203.74, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:12.838Z", - "updatedAt": "2021-05-09T21:33:12.838Z" + "createdAt": "2021-05-09T21:33:14.202Z", + "updatedAt": "2021-05-09T21:33:14.202Z" }, { - "id": "71750282-0ffe-46ed-b8c2-37e36c148833", + "id": "d36afd1d-1f76-4f2d-b630-69bf85796496", "workPeriodId": "91fcf91f-b2cf-4909-8f03-b5efc0732b28", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 203.74, + "amount": 494.46, "status": "cancelled", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:14.202Z", - "updatedAt": "2021-05-09T21:33:14.202Z" + "createdAt": "2021-05-09T21:33:12.838Z", + "updatedAt": "2021-05-09T21:33:12.838Z" } ] }, @@ -2051,6 +2602,4600 @@ ] } ] + }, + { + "id": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "projectId": 16714, + "userId": "4b00d029-c87b-47b2-bfe2-0ab80d8b5774", + "jobId": "fc0240f0-8c8f-40ce-a551-e83b45673098", + "status": "sourcing", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 106.51, + "customerRate": 296.66, + "rateType": "daily", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-25T13:10:26.146Z", + "updatedAt": "2021-05-30T11:41:11.436Z", + "workPeriods": [ + { + "id": "f5ab8f98-7e4b-4cf9-a1e5-6a15ab6bad91", + "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "userHandle": "nkumar2", + "projectId": 16714, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 63.94, + "customerRate": 16.07, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:12.251Z", + "updatedAt": "2021-05-30T16:02:51.923Z", + "payments": [] + }, + { + "id": "c20620e6-17de-41f5-8a0a-089683550b2f", + "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "userHandle": "nkumar2", + "projectId": 16714, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 248.87, + "customerRate": 217.32, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:12.260Z", + "updatedAt": "2021-05-30T16:03:39.019Z", + "payments": [] + }, + { + "id": "5c01c872-eb1e-4161-8e54-791f4ffe7bba", + "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "userHandle": "nkumar2", + "projectId": 16714, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 248.87, + "customerRate": 281.39, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:12.223Z", + "updatedAt": "2021-05-30T16:04:56.201Z", + "payments": [] + }, + { + "id": "1073cd3c-8fc3-4642-b71b-ed49e3628952", + "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "userHandle": "nkumar2", + "projectId": 16714, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 196.23, + "customerRate": 271.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:41:12.278Z", + "updatedAt": "2021-05-30T16:12:06.800Z", + "payments": [] + }, + { + "id": "1d912726-4d5e-4063-9651-bcf51fd4f42e", + "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "userHandle": "nkumar2", + "projectId": 16714, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 63.04, + "customerRate": 47.87, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:12.274Z", + "updatedAt": "2021-05-30T16:05:47.699Z", + "payments": [] + }, + { + "id": "15cbf62c-67d9-4f4a-92dc-546cb0afac32", + "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "userHandle": "nkumar2", + "projectId": 16714, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 295.62, + "customerRate": 235.7, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:41:12.216Z", + "updatedAt": "2021-05-30T16:12:06.801Z", + "payments": [] + } + ] + }, + { + "id": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "projectId": 17091, + "userId": "b870a229-e954-482d-b270-a39fcf2aec53", + "jobId": "fb8b92f6-4ffb-4ba6-8c38-c2d4a151f76b", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 4.07, + "customerRate": 132.43, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:53:14.352Z", + "updatedAt": "2021-05-30T11:48:17.788Z", + "workPeriods": [ + { + "id": "73224384-4ac4-402e-8c6e-e04c64ef34da", + "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "userHandle": "jimsun", + "projectId": 17091, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 8.1, + "customerRate": 155.91, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:18.557Z", + "updatedAt": "2021-05-30T16:11:38.205Z", + "payments": [] + }, + { + "id": "bcadfe78-7237-45e9-bd77-cba366ef9cf5", + "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "userHandle": "jimsun", + "projectId": 17091, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 216.18, + "customerRate": 262.91, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:18.549Z", + "updatedAt": "2021-05-30T16:03:44.298Z", + "payments": [] + }, + { + "id": "31436a53-4c2e-40fc-98f2-2b6e3e36aa2b", + "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "userHandle": "jimsun", + "projectId": 17091, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": null, + "memberRate": 104.85, + "customerRate": 225.57, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:18.565Z", + "updatedAt": "2021-05-30T16:11:38.208Z", + "payments": [] + }, + { + "id": "b04da80e-eff3-4be3-8849-f39f6af417b9", + "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "userHandle": "jimsun", + "projectId": 17091, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 131.12, + "customerRate": 270.74, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:18.561Z", + "updatedAt": "2021-05-30T16:03:59.366Z", + "payments": [] + }, + { + "id": "788dae21-92c8-4ae8-9e10-4e394f22cb73", + "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "userHandle": "jimsun", + "projectId": 17091, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 34.56, + "customerRate": 265.36, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:18.554Z", + "updatedAt": "2021-05-30T16:04:28.730Z", + "payments": [] + }, + { + "id": "eaffd373-e0f8-4798-ae42-24fb9bebeef3", + "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "userHandle": "jimsun", + "projectId": 17091, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 30.95, + "customerRate": 54.36, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:18.552Z", + "updatedAt": "2021-05-30T16:03:06.211Z", + "payments": [] + } + ] + }, + { + "id": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", + "projectId": 16781, + "userId": "7eea7c2f-5a46-4646-82bd-db4ac528378d", + "jobId": "ff3feeae-d4f7-457c-bff7-215be5efe2b8", + "status": "sourcing", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 61.33, + "customerRate": 196.21, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-12T10:49:14.209Z", + "updatedAt": "2021-05-30T11:48:22.088Z", + "workPeriods": [ + { + "id": "41217a15-4231-480c-91bc-492cbbe95113", + "resourceBookingId": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", + "userHandle": "ZeroChance", + "projectId": 16781, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 217.78, + "customerRate": 134.84, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:22.909Z", + "updatedAt": "2021-05-30T16:11:40.871Z", + "payments": [] + }, + { + "id": "cd2ff33c-70d9-4b47-a1f2-d3a32febb22d", + "resourceBookingId": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", + "userHandle": "ZeroChance", + "projectId": 16781, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 297.89, + "customerRate": 277.1, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:22.945Z", + "updatedAt": "2021-05-30T16:03:28.495Z", + "payments": [] + }, + { + "id": "354f7ab0-b9e3-4afd-a082-0ef0bc02ae44", + "resourceBookingId": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", + "userHandle": "ZeroChance", + "projectId": 16781, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 4, + "memberRate": 197.07, + "customerRate": 93.25, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:22.902Z", + "updatedAt": "2021-05-30T16:05:23.620Z", + "payments": [] + }, + { + "id": "e3edac07-0c38-4f5a-a62d-b1fcc037e6c9", + "resourceBookingId": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", + "userHandle": "ZeroChance", + "projectId": 16781, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 115.09, + "customerRate": 282.56, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:22.907Z", + "updatedAt": "2021-05-30T16:03:11.591Z", + "payments": [] + }, + { + "id": "4703c8d8-7e7b-4ee3-8cea-08c3d15de835", + "resourceBookingId": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", + "userHandle": "ZeroChance", + "projectId": 16781, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 175.62, + "customerRate": 101, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:22.912Z", + "updatedAt": "2021-05-30T16:05:12.956Z", + "payments": [] + } + ] + }, + { + "id": "d2f7dc2e-bed6-4549-b6c6-0616840782fb", + "projectId": 16762, + "userId": "595edbcd-f4d1-468e-a422-50bf61e2fa87", + "jobId": "fe481d1c-cf87-49c1-9370-695f9f754041", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 66, + "customerRate": 146.2, + "rateType": "daily", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-12T10:58:55.040Z", + "updatedAt": "2021-05-30T11:48:26.483Z", + "workPeriods": [ + { + "id": "f040a900-cd3e-4ebf-9a52-10db19e90e83", + "resourceBookingId": "d2f7dc2e-bed6-4549-b6c6-0616840782fb", + "userHandle": "sachin-kumar", + "projectId": 16762, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 178.01, + "customerRate": 282.9, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:27.249Z", + "updatedAt": "2021-05-30T16:02:59.987Z", + "payments": [] + }, + { + "id": "ecb9c0a7-0438-482d-9c8b-6ad6ffba2586", + "resourceBookingId": "d2f7dc2e-bed6-4549-b6c6-0616840782fb", + "userHandle": "sachin-kumar", + "projectId": 16762, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 148.65, + "customerRate": 131.63, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:27.266Z", + "updatedAt": "2021-05-30T16:03:04.382Z", + "payments": [] + }, + { + "id": "98f6ced4-7a27-4dd7-bf51-5bba41091f03", + "resourceBookingId": "d2f7dc2e-bed6-4549-b6c6-0616840782fb", + "userHandle": "sachin-kumar", + "projectId": 16762, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 69.98, + "customerRate": 245.82, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:27.256Z", + "updatedAt": "2021-05-30T16:04:13.737Z", + "payments": [] + }, + { + "id": "02281a71-46c3-4850-a243-737616ef78dc", + "resourceBookingId": "d2f7dc2e-bed6-4549-b6c6-0616840782fb", + "userHandle": "sachin-kumar", + "projectId": 16762, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 28.27, + "customerRate": 154.51, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:27.259Z", + "updatedAt": "2021-05-30T16:11:43.711Z", + "payments": [] + }, + { + "id": "248b0c82-8f49-482c-bd71-f3127acdefd8", + "resourceBookingId": "d2f7dc2e-bed6-4549-b6c6-0616840782fb", + "userHandle": "sachin-kumar", + "projectId": 16762, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 118.86, + "customerRate": 300.07, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:27.311Z", + "updatedAt": "2021-05-30T16:05:37.170Z", + "payments": [] + } + ] + }, + { + "id": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "projectId": 16870, + "userId": "b851684b-1071-47c3-8719-bdae96aa0e6d", + "jobId": "fed687e1-4257-48bb-806c-38712f9bf14f", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 115.29, + "customerRate": 278.45, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-04-29T09:50:13.905Z", + "updatedAt": "2021-05-30T11:48:15.196Z", + "workPeriods": [ + { + "id": "e95f0541-bc37-4c1e-a619-029753c1a69a", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 43.3, + "customerRate": 40.52, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:16.045Z", + "updatedAt": "2021-05-30T16:03:08.035Z", + "payments": [] + }, + { + "id": "cb6114ca-1624-4239-8243-aee05f8b5fe5", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 216.67, + "customerRate": 260.18, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:16.042Z", + "updatedAt": "2021-05-30T16:03:29.374Z", + "payments": [] + }, + { + "id": "f382c2c8-fe32-42f3-b960-29c345ab0264", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 185.3, + "customerRate": 203.04, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:16.004Z", + "updatedAt": "2021-05-30T16:11:36.370Z", + "payments": [] + }, + { + "id": "9215b576-8f67-4511-88f9-76000d6c8326", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 283.73, + "customerRate": 232.51, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:16.008Z", + "updatedAt": "2021-05-30T16:04:19.062Z", + "payments": [] + }, + { + "id": "333bcf25-25cc-4a4e-a6d7-2666a1326d68", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 162.3, + "customerRate": 132.28, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:16.001Z", + "updatedAt": "2021-05-30T16:05:26.263Z", + "payments": [] + } + ] + }, + { + "id": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "projectId": 17232, + "userId": "1ab93e53-71f6-4c50-ab48-9446229b6451", + "jobId": "ff76b81d-f49b-4019-b50e-c7932a818f19", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 18.4, + "customerRate": 84.88, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-17T13:12:55.459Z", + "updatedAt": "2021-05-30T11:48:11.067Z", + "workPeriods": [ + { + "id": "e84de57c-d131-4431-8e46-9452218d30e7", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 253.31, + "customerRate": 57.88, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:12.550Z", + "updatedAt": "2021-05-30T16:03:08.981Z", + "payments": [] + }, + { + "id": "97dabae3-74dd-45f6-ab83-53e4a828c4a6", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 297.19, + "customerRate": 280.07, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:12.488Z", + "updatedAt": "2021-05-30T16:11:34.545Z", + "payments": [] + }, + { + "id": "75c345fa-9e34-46a9-8cf0-46245495110d", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 236.08, + "customerRate": 26.72, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:12.586Z", + "updatedAt": "2021-05-30T16:04:33.475Z", + "payments": [] + }, + { + "id": "20242864-9dd6-4376-85fb-1402297e4597", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 214.14, + "customerRate": 67.44, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:12.497Z", + "updatedAt": "2021-05-30T16:05:41.521Z", + "payments": [] + }, + { + "id": "c0c613e3-845a-45c0-85fe-41c063d9df3d", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 33.79, + "customerRate": 282.9, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:12.493Z", + "updatedAt": "2021-05-30T16:11:34.546Z", + "payments": [] + }, + { + "id": "b53b6f48-0a07-434e-a03d-f5d9ab772e60", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 4, + "memberRate": 58.11, + "customerRate": 256.06, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:12.515Z", + "updatedAt": "2021-05-30T16:03:53.254Z", + "payments": [] + } + ] + }, + { + "id": "f667a667-6026-4d93-89bb-358aced982e5", + "projectId": 16870, + "userId": "2bba34d5-20e4-46d6-bfc1-05736b17afbb", + "jobId": "fed687e1-4257-48bb-806c-38712f9bf14f", + "status": "placed", + "startDate": "2021-01-03", + "endDate": "2021-02-03", + "memberRate": 61.4, + "customerRate": 114.05, + "rateType": "daily", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-04-29T09:28:04.405Z", + "updatedAt": "2021-05-30T11:48:13.500Z", + "workPeriods": [ + { + "id": "1dd649b8-f536-4285-989d-56c45f1fca4d", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": null, + "memberRate": 21.8, + "customerRate": 54.36, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:14.325Z", + "updatedAt": "2021-05-30T16:05:45.949Z", + "payments": [] + }, + { + "id": "d44b8f1a-46b7-43a8-afd6-d2d13bd02fa5", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 298.88, + "customerRate": 84.09, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:14.367Z", + "updatedAt": "2021-05-30T16:03:23.275Z", + "payments": [] + }, + { + "id": "7011b330-8509-4f60-a2db-c0f5c9b5837b", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 3, + "memberRate": 47.41, + "customerRate": 18.13, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:14.320Z", + "updatedAt": "2021-05-30T16:11:35.512Z", + "payments": [] + }, + { + "id": "8252cd13-4351-4a86-9315-521963f329f5", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 63.04, + "customerRate": 209, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:14.322Z", + "updatedAt": "2021-05-30T16:04:23.465Z", + "payments": [] + }, + { + "id": "259d8de1-1c56-49d0-936d-d5fcbdcc5a8a", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": null, + "memberRate": 162.12, + "customerRate": 99.32, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:14.314Z", + "updatedAt": "2021-05-30T16:05:34.486Z", + "payments": [] + } + ] + }, + { + "id": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "projectId": 17103, + "userId": "fa5f4dc4-2992-4066-b4cc-16ceb5d1c1b7", + "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 287.14, + "customerRate": 258.37, + "rateType": "daily", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-02T06:00:42.366Z", + "updatedAt": "2021-05-30T11:48:23.808Z", + "workPeriods": [ + { + "id": "b60da405-cee8-41c9-919b-98d9acfd9f74", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", + "projectId": 17103, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 271.31, + "customerRate": 195.92, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:24.607Z", + "updatedAt": "2021-05-30T16:03:52.385Z", + "payments": [] + }, + { + "id": "19110441-201e-449b-a350-661c50fb1387", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", + "projectId": 17103, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 30.12, + "customerRate": 84.76, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:24.639Z", + "updatedAt": "2021-05-30T16:05:49.510Z", + "payments": [] + }, + { + "id": "1f12ada9-d0e6-43df-abe5-8a78850b20b4", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", + "projectId": 17103, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 191.92, + "customerRate": 6.17, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:24.601Z", + "updatedAt": "2021-05-30T16:11:41.938Z", + "payments": [] + }, + { + "id": "1e94f892-ae25-4233-b9b0-81aa70c00b1e", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", + "projectId": 17103, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 109.93, + "customerRate": 271.9, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:24.604Z", + "updatedAt": "2021-05-30T16:05:45.069Z", + "payments": [] + }, + { + "id": "4d7f0350-b2f0-4f31-acc3-6555c3756fdd", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", + "projectId": 17103, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 70.84, + "customerRate": 207.33, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:24.637Z", + "updatedAt": "2021-05-30T16:05:08.370Z", + "payments": [] + } + ] + }, + { + "id": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "projectId": 16870, + "userId": "60d3e956-820b-4d59-a30b-9309b838fac5", + "jobId": "fe539bef-9119-4a8c-b7b0-915e7e3a3ba3", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 60.63, + "customerRate": 132.43, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-04-30T08:09:51.618Z", + "updatedAt": "2021-05-30T11:48:20.311Z", + "workPeriods": [ + { + "id": "6ad492cf-4d90-4608-8dd5-13aaafad12e2", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 131.12, + "customerRate": 18.28, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:21.194Z", + "updatedAt": "2021-05-30T16:11:39.976Z", + "payments": [] + }, + { + "id": "61134c36-5e69-469c-bc50-75648f7949ca", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 112.54, + "customerRate": 222.98, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:21.187Z", + "updatedAt": "2021-05-30T16:04:51.659Z", + "payments": [] + }, + { + "id": "f095424c-9a15-4f37-b8e4-1cd685f17451", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 200.17, + "customerRate": 260.87, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:21.198Z", + "updatedAt": "2021-05-30T16:11:39.977Z", + "payments": [] + }, + { + "id": "f79b0a83-5f72-4e9c-bffe-4ebf694db7f4", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 27.09, + "customerRate": 40.76, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:21.107Z", + "updatedAt": "2021-05-30T16:02:50.987Z", + "payments": [] + }, + { + "id": "c75ee2eb-a7c0-4eb8-83f9-860ce22d1b03", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 278.4, + "customerRate": 270.68, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:21.104Z", + "updatedAt": "2021-05-30T16:03:34.616Z", + "payments": [] + }, + { + "id": "b0aad7c5-bafb-4cae-90ca-832334505e9b", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 155.75, + "customerRate": 13.09, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:21.192Z", + "updatedAt": "2021-05-30T16:03:58.410Z", + "payments": [] + } + ] + }, + { + "id": "016312f6-72cf-486b-be8f-956ca4b2171e", + "projectId": 16706, + "userId": "4b00d029-c87b-47b2-bfe2-0ab80d8b5774", + "jobId": "fc2b006d-997b-49c3-a414-59ee54a48f9f", + "status": "sourcing", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 114.33, + "customerRate": 67.93, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-26T08:27:38.010Z", + "updatedAt": "2021-05-30T11:40:20.936Z", + "workPeriods": [ + { + "id": "6629d501-d17f-4261-a4d1-ed51d2a4b533", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 181.55, + "customerRate": 22.69, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:40:21.784Z", + "updatedAt": "2021-05-30T16:04:46.695Z", + "payments": [] + }, + { + "id": "42ca2389-e8a6-42c5-8a97-8d47531d2f23", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 41.26, + "customerRate": 286.6, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:40:21.766Z", + "updatedAt": "2021-05-30T16:05:14.728Z", + "payments": [] + }, + { + "id": "a2e2905a-efd7-45ba-a891-f0523b4b1351", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": null, + "memberRate": 114.59, + "customerRate": 180.76, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:40:21.794Z", + "updatedAt": "2021-05-30T16:04:08.375Z", + "payments": [] + }, + { + "id": "b3101b68-cc83-4a5c-aa33-0c5220e4b78f", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 280.73, + "customerRate": 7.02, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:40:21.780Z", + "updatedAt": "2021-05-30T16:12:19.637Z", + "payments": [] + }, + { + "id": "149f2401-f2c2-4e3f-98fe-44148820cd5e", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 3.94, + "customerRate": 88.91, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:40:21.757Z", + "updatedAt": "2021-05-30T16:12:19.636Z", + "payments": [] + }, + { + "id": "e002f23e-8358-4f49-8770-af19aa23708e", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 4, + "memberRate": 108.35, + "customerRate": 189.61, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:40:21.710Z", + "updatedAt": "2021-05-30T16:03:14.321Z", + "payments": [] + } + ] + }, + { + "id": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "projectId": 16899, + "userId": "5bc40e16-4fdb-40f1-93fe-de465789e1b2", + "jobId": "fe270791-bc24-4f6a-8c1b-b897f5d97d2f", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 271.93, + "customerRate": 102.37, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:45:15.939Z", + "updatedAt": "2021-05-30T11:48:39.054Z", + "workPeriods": [ + { + "id": "eeea5cf0-513c-4d1a-9318-a376aa86c28f", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 298.22, + "customerRate": 277.1, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:39.879Z", + "updatedAt": "2021-05-30T16:03:01.733Z", + "payments": [] + }, + { + "id": "ab2c5ad4-165f-48d2-bf1c-005b56e049ce", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 174.63, + "customerRate": 103.63, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:39.824Z", + "updatedAt": "2021-05-30T16:11:51.073Z", + "payments": [] + }, + { + "id": "aed553d8-4eb4-45a8-86f0-3c21f81c7570", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 19.08, + "customerRate": 101.27, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:39.870Z", + "updatedAt": "2021-05-30T16:04:00.297Z", + "payments": [] + }, + { + "id": "4c9a7d50-8014-4ff2-9867-98dc66e466ac", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 209.34, + "customerRate": 97.01, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:39.838Z", + "updatedAt": "2021-05-30T16:05:09.308Z", + "payments": [] + }, + { + "id": "d5c4716f-e26a-4d3f-a57d-2410d7537ecd", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": null, + "memberRate": 30.95, + "customerRate": 156.32, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:39.820Z", + "updatedAt": "2021-05-30T16:03:21.454Z", + "payments": [] + } + ] + }, + { + "id": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "projectId": 16870, + "userId": "2bba34d5-20e4-46d6-bfc1-05736b17afbb", + "jobId": "fe539bef-9119-4a8c-b7b0-915e7e3a3ba3", + "status": "closed", + "startDate": "2021-01-02", + "endDate": "2021-02-02", + "memberRate": 18.4, + "customerRate": 30.14, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T03:43:44.374Z", + "updatedAt": "2021-05-30T11:49:00.956Z", + "workPeriods": [ + { + "id": "b9768ae4-ae40-4bb9-8bc2-970c9f36f0bf", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 0, + "memberRate": 213.59, + "customerRate": 286.15, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:01.820Z", + "updatedAt": "2021-05-30T16:12:04.121Z", + "payments": [] + }, + { + "id": "77ac0f07-d4ae-463e-a9db-437623a29958", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 191.95, + "customerRate": 172.18, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:01.793Z", + "updatedAt": "2021-05-30T16:04:31.663Z", + "payments": [] + }, + { + "id": "1f5d3abe-fe2b-4961-831c-b7bbdf76da82", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 54.98, + "customerRate": 237.88, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:01.732Z", + "updatedAt": "2021-05-30T16:05:43.321Z", + "payments": [] + }, + { + "id": "1c526f5f-d9e0-4e16-8421-cee4e8154a3c", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 36.75, + "customerRate": 21.33, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:01.802Z", + "updatedAt": "2021-05-30T16:05:48.592Z", + "payments": [] + }, + { + "id": "b7bce7db-65dc-4447-8865-f6e8a84a867e", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 209.34, + "customerRate": 251.36, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:01.698Z", + "updatedAt": "2021-05-30T16:03:48.778Z", + "payments": [] + }, + { + "id": "a475c2fa-c5ce-4a30-bdd8-d4fad1e6308c", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 2, + "memberRate": 25.99, + "customerRate": 155.48, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:01.795Z", + "updatedAt": "2021-05-30T16:12:04.120Z", + "payments": [] + } + ] + }, + { + "id": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "projectId": 17290, + "userId": "0eaf032f-f376-47cc-b7aa-668685efac90", + "jobId": "fe600350-0a6d-4dac-922f-a6a7d285daa1", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 106.51, + "customerRate": 111.21, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-18T08:18:13.123Z", + "updatedAt": "2021-05-30T11:48:52.915Z", + "workPeriods": [ + { + "id": "e6b70714-8bd6-47ce-9e58-f04d3c25ee28", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 147.9, + "customerRate": 132.28, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:53.719Z", + "updatedAt": "2021-05-30T16:03:09.841Z", + "payments": [] + }, + { + "id": "b6c1f079-e5cb-46a0-a6bf-5988ec013c4c", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 22.66, + "customerRate": 265.36, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:53.776Z", + "updatedAt": "2021-05-30T16:03:49.745Z", + "payments": [] + }, + { + "id": "064d2511-9af6-4d6a-be4f-79eebacc6345", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 66.85, + "customerRate": 16.02, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:53.778Z", + "updatedAt": "2021-05-30T16:11:59.550Z", + "payments": [] + }, + { + "id": "c8c69543-1598-43e4-9ef6-8a569ebdf831", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 252.02, + "customerRate": 271.75, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:53.716Z", + "updatedAt": "2021-05-30T16:11:59.551Z", + "payments": [] + }, + { + "id": "0d237fa9-3fe9-48dc-82b8-7027edddc5a1", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 33.79, + "customerRate": 155.11, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:53.731Z", + "updatedAt": "2021-05-30T16:05:57.480Z", + "payments": [] + }, + { + "id": "7bc96a71-deda-4cde-b8b0-f809cea1398a", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 140.77, + "customerRate": 120.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:53.710Z", + "updatedAt": "2021-05-30T16:04:26.082Z", + "payments": [] + } + ] + }, + { + "id": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "projectId": 17290, + "userId": "1f6ca39c-0620-4de0-9bb2-d64d4ce26b42", + "jobId": "fe600350-0a6d-4dac-922f-a6a7d285daa1", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 4.07, + "customerRate": 296.66, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-18T07:05:53.664Z", + "updatedAt": "2021-05-30T11:48:33.177Z", + "workPeriods": [ + { + "id": "e29735b3-cf81-4877-8fd5-6d346a1824f0", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 8.1, + "customerRate": 273.89, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:33.942Z", + "updatedAt": "2021-05-30T16:03:12.511Z", + "payments": [] + }, + { + "id": "df0b3604-7ee9-4862-a8d1-abe8a2142f77", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 154, + "customerRate": 244.09, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:33.954Z", + "updatedAt": "2021-05-30T16:03:15.193Z", + "payments": [] + }, + { + "id": "9a480e44-2026-4327-a220-715ace30743e", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 248.36, + "customerRate": 257.04, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:33.952Z", + "updatedAt": "2021-05-30T16:04:12.811Z", + "payments": [] + }, + { + "id": "b6147962-6666-4534-8b73-0c7f9a7052e8", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 126.28, + "customerRate": 4.22, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:33.959Z", + "updatedAt": "2021-05-30T16:11:47.528Z", + "payments": [] + }, + { + "id": "47ac2474-d9e9-416d-afa8-fea8fb4f2a6c", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 28.27, + "customerRate": 256.06, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:33.940Z", + "updatedAt": "2021-05-30T16:05:12.030Z", + "payments": [] + } + ] + }, + { + "id": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "projectId": 16781, + "userId": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "jobId": "ff3feeae-d4f7-457c-bff7-215be5efe2b8", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 54.02, + "customerRate": 217.99, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-12T15:02:15.210Z", + "updatedAt": "2021-05-30T11:48:59.149Z", + "workPeriods": [ + { + "id": "d062e2fe-2446-4fb5-b0b3-0577cd57fd7c", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 122.15, + "customerRate": 115.26, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:59.909Z", + "updatedAt": "2021-05-30T16:03:24.198Z", + "payments": [] + }, + { + "id": "bdd60068-9e8d-4c43-8440-48a066ba4396", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 15.41, + "customerRate": 55.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:59.921Z", + "updatedAt": "2021-05-30T16:12:03.178Z", + "payments": [] + }, + { + "id": "a0eec246-3b4d-41b6-9d54-b892513bd727", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 231.38, + "customerRate": 270.68, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:00.007Z", + "updatedAt": "2021-05-30T16:04:10.173Z", + "payments": [] + }, + { + "id": "752f3cf3-a9c2-487c-93ae-22d21af10403", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 66.85, + "customerRate": 42.45, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:00.041Z", + "updatedAt": "2021-05-30T16:04:34.403Z", + "payments": [] + }, + { + "id": "5be5a577-32cd-4557-8d11-bb5723dd7be2", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 158.5, + "customerRate": 299.34, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:59.998Z", + "updatedAt": "2021-05-30T16:04:57.065Z", + "payments": [] + }, + { + "id": "e9ff7b7f-05ec-45f9-a09e-b2c24017b59b", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 34.56, + "customerRate": 175.32, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:59.974Z", + "updatedAt": "2021-05-30T16:12:03.180Z", + "payments": [] + } + ] + }, + { + "id": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "projectId": 17363, + "userId": "c40cdb0a-4fac-4ca1-8052-d92001858887", + "jobId": "fd48d96e-b0f2-43b7-8a48-f4fa194d6bc8", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 235.26, + "customerRate": 84.88, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "71c5e6a8-51d9-4fb5-91ce-d974642531af", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-28T04:41:39.728Z", + "updatedAt": "2021-05-30T11:48:34.793Z", + "workPeriods": [ + { + "id": "94a897e9-6291-4206-a0b1-74c35ff06a6e", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": null, + "memberRate": 214.34, + "customerRate": 16.02, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:35.596Z", + "updatedAt": "2021-05-30T16:04:17.285Z", + "payments": [] + }, + { + "id": "2570737c-02b4-4b90-b692-45dcc5774215", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 34.23, + "customerRate": 257.04, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:35.576Z", + "updatedAt": "2021-05-30T16:11:48.431Z", + "payments": [] + }, + { + "id": "789b83d2-3278-4c90-a5c1-3aa96e61db4b", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 181.55, + "customerRate": 97.01, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:35.600Z", + "updatedAt": "2021-05-30T16:04:27.884Z", + "payments": [] + }, + { + "id": "764fec45-842e-4d06-b009-29ba4c9c116c", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 197.07, + "customerRate": 39.93, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:35.588Z", + "updatedAt": "2021-05-30T16:04:32.547Z", + "payments": [] + }, + { + "id": "00fa978f-e2c4-4cab-8da9-e6b0dce80258", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 34.07, + "customerRate": 287.29, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:35.585Z", + "updatedAt": "2021-05-30T16:06:04.649Z", + "payments": [] + }, + { + "id": "daa54d7d-84f5-4f62-b58c-4c09ec26dd1a", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 297.19, + "customerRate": 32.3, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:35.581Z", + "updatedAt": "2021-05-30T16:11:48.432Z", + "payments": [] + } + ] + }, + { + "id": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "projectId": 17103, + "userId": "d8e11333-af08-4149-a270-b355001b44e7", + "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 240.84, + "customerRate": 100.25, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T12:20:21.613Z", + "updatedAt": "2021-05-30T11:48:28.143Z", + "workPeriods": [ + { + "id": "d0571318-a0be-4263-bfe9-eba783ba8957", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 205.68, + "customerRate": 154.12, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:28.918Z", + "updatedAt": "2021-05-30T16:03:25.073Z", + "payments": [] + }, + { + "id": "9fd57023-518e-42e3-a998-6098ae49a3fb", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 4, + "memberRate": 202.33, + "customerRate": 1.49, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:28.905Z", + "updatedAt": "2021-05-30T16:04:11.041Z", + "payments": [] + }, + { + "id": "b473e949-0a6e-452c-b626-6cc79ee61d1c", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 146.28, + "customerRate": 145.74, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:28.924Z", + "updatedAt": "2021-05-30T16:03:54.140Z", + "payments": [] + }, + { + "id": "b952458d-12a9-4398-b549-3fe44d3814dd", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 214.34, + "customerRate": 167.08, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:28.922Z", + "updatedAt": "2021-05-30T16:11:44.684Z", + "payments": [] + }, + { + "id": "f206743e-f797-40a8-8dee-634fb0f08e08", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 184.32, + "customerRate": 255.03, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:28.873Z", + "updatedAt": "2021-05-30T16:02:57.423Z", + "payments": [] + }, + { + "id": "ef0f5c89-c979-4504-9b20-54b110a1495a", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 166.73, + "customerRate": 189.61, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:28.915Z", + "updatedAt": "2021-05-30T16:11:44.683Z", + "payments": [] + } + ] + }, + { + "id": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "projectId": 16899, + "userId": "47034de0-698d-4e1b-a10b-ae4b8c59288e", + "jobId": "fe270791-bc24-4f6a-8c1b-b897f5d97d2f", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 4.07, + "customerRate": 258.37, + "rateType": "monthly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:36:02.161Z", + "updatedAt": "2021-05-30T11:48:31.516Z", + "workPeriods": [ + { + "id": "0ae1abc8-3e8d-4eea-933e-ab3cfa7c6826", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 122.15, + "customerRate": 25.64, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:32.269Z", + "updatedAt": "2021-05-30T16:11:46.620Z", + "payments": [] + }, + { + "id": "97fd7b67-0cd7-47fb-9017-7a7b653ef940", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 298.22, + "customerRate": 271.9, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:32.256Z", + "updatedAt": "2021-05-30T16:04:14.644Z", + "payments": [] + }, + { + "id": "3fe04a0d-62ff-4060-935a-4f294ee19fac", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 280.11, + "customerRate": 195.99, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:32.237Z", + "updatedAt": "2021-05-30T16:11:46.621Z", + "payments": [] + }, + { + "id": "a3daf653-c87c-451b-acfc-508ef8364fbb", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 207.81, + "customerRate": 297.59, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:32.273Z", + "updatedAt": "2021-05-30T16:04:05.668Z", + "payments": [] + }, + { + "id": "15337715-0c4a-4036-a1d3-5aaa14a214af", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 104.85, + "customerRate": 260.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:32.252Z", + "updatedAt": "2021-05-30T16:05:51.388Z", + "payments": [] + }, + { + "id": "304a0ba1-2534-46c0-b2e9-9d85936755df", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 129.72, + "customerRate": 93.25, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:32.234Z", + "updatedAt": "2021-05-30T16:05:28.904Z", + "payments": [] + } + ] + }, + { + "id": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "projectId": 16706, + "userId": "149c9ad0-f5d7-4192-8c61-f634f6120816", + "jobId": "fc2b006d-997b-49c3-a414-59ee54a48f9f", + "status": "sourcing", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 201.77, + "customerRate": 84.88, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-26T08:27:38.944Z", + "updatedAt": "2021-05-30T11:41:01.828Z", + "workPeriods": [ + { + "id": "b21eed94-dce5-42e1-9734-155594773222", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 186.09, + "customerRate": 37.15, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:02.603Z", + "updatedAt": "2021-05-30T16:03:56.759Z", + "payments": [] + }, + { + "id": "85201917-77e0-41d1-8c76-66eefcbc23e0", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 143.95, + "customerRate": 7.02, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:41:02.617Z", + "updatedAt": "2021-05-30T16:11:54.733Z", + "payments": [] + }, + { + "id": "56105e62-d690-484c-9fe8-b6d18671c9ac", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 127.23, + "customerRate": 162.5, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:02.600Z", + "updatedAt": "2021-05-30T16:04:59.664Z", + "payments": [] + }, + { + "id": "5d6c9cf8-a6bc-43b9-af35-67812fc10db9", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 140.77, + "customerRate": 161.96, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:02.595Z", + "updatedAt": "2021-05-30T16:04:54.450Z", + "payments": [] + }, + { + "id": "34ab1933-c9a3-4087-b220-3716d3729703", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 202.33, + "customerRate": 140.66, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:02.645Z", + "updatedAt": "2021-05-30T16:05:24.577Z", + "payments": [] + } + ] + }, + { + "id": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "projectId": 16714, + "userId": "bde2cf99-b290-40cd-a064-9b6bb7e54bea", + "jobId": "fc0240f0-8c8f-40ce-a551-e83b45673098", + "status": "sourcing", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 115.29, + "customerRate": 84.88, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-26T08:16:00.294Z", + "updatedAt": "2021-05-30T11:48:40.735Z", + "workPeriods": [ + { + "id": "ae316c25-ec04-4bc1-b209-798b69ed5250", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 34.07, + "customerRate": 55.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:41.592Z", + "updatedAt": "2021-05-30T16:04:01.125Z", + "payments": [] + }, + { + "id": "7501c035-46c4-4704-973a-595145bd2d17", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 114.76, + "customerRate": 268.61, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:41.588Z", + "updatedAt": "2021-05-30T16:11:52.009Z", + "payments": [] + }, + { + "id": "519b4818-8e39-4d4c-bcc6-3486816a4bc8", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 173.56, + "customerRate": 140.66, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:41.595Z", + "updatedAt": "2021-05-30T16:05:04.624Z", + "payments": [] + }, + { + "id": "13444e2f-1254-40fc-a7d9-08ca49fc7239", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 276.5, + "customerRate": 287.29, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:41.597Z", + "updatedAt": "2021-05-30T16:05:53.017Z", + "payments": [] + }, + { + "id": "66c66f68-3882-4521-9c4e-ce08e6b47ca0", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 4, + "memberRate": 43.3, + "customerRate": 72.49, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:41.579Z", + "updatedAt": "2021-05-30T16:04:44.142Z", + "payments": [] + } + ] + }, + { + "id": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "projectId": 16805, + "userId": "13330208-ab10-4ca3-9fd1-a132fbf7ac4e", + "jobId": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 4.79, + "customerRate": 146.2, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-19T11:04:22.566Z", + "updatedAt": "2021-05-30T11:48:46.861Z", + "workPeriods": [ + { + "id": "f2a85b87-8e75-4db9-b29c-a3c170101ed6", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", + "projectId": 16805, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 104.6, + "customerRate": 217.32, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:47.631Z", + "updatedAt": "2021-05-30T16:02:54.605Z", + "payments": [] + }, + { + "id": "e6801711-327e-4387-a7f0-d592b49b1ba3", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", + "projectId": 16805, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 54.82, + "customerRate": 191.42, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:47.726Z", + "updatedAt": "2021-05-30T16:03:10.690Z", + "payments": [] + }, + { + "id": "cd44a405-c77a-4f6d-95a7-c10de740eb29", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", + "projectId": 16805, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 70.98, + "customerRate": 107.72, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:47.728Z", + "updatedAt": "2021-05-30T16:03:27.679Z", + "payments": [] + }, + { + "id": "6565d172-61c4-4357-a01f-b8e6bf179e2f", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", + "projectId": 16805, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 172.94, + "customerRate": 67.44, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:47.638Z", + "updatedAt": "2021-05-30T16:11:55.746Z", + "payments": [] + }, + { + "id": "f7c8a1e8-76c6-4eee-bb99-15cce5cb8b1f", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", + "projectId": 16805, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 219.84, + "customerRate": 122.72, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:47.648Z", + "updatedAt": "2021-05-30T16:11:55.747Z", + "payments": [] + }, + { + "id": "a71105ef-a2b8-4163-b2de-9ddcaa8329bb", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", + "projectId": 16805, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 252.02, + "customerRate": 219.79, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:47.731Z", + "updatedAt": "2021-05-30T16:04:02.955Z", + "payments": [] + } + ] + }, + { + "id": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "projectId": 17232, + "userId": "8e6bfd51-fd78-45fa-9234-172976168f29", + "jobId": "ff76b81d-f49b-4019-b50e-c7932a818f19", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 225.34, + "customerRate": 170.64, + "rateType": "monthly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-18T04:01:23.725Z", + "updatedAt": "2021-05-30T11:48:44.244Z", + "workPeriods": [ + { + "id": "eeda07cc-8a10-4337-ab8d-e1107e4072a0", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 204.06, + "customerRate": 143.1, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:45.002Z", + "updatedAt": "2021-05-30T16:03:02.635Z", + "payments": [] + }, + { + "id": "7aaa313f-62be-4483-80b2-87a95b4a0b8c", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 185.82, + "customerRate": 286.29, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:45.006Z", + "updatedAt": "2021-05-30T16:04:26.995Z", + "payments": [] + }, + { + "id": "c0568bd9-1523-494d-bb3b-4b7a89026de9", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 248.86, + "customerRate": 271.75, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:45.074Z", + "updatedAt": "2021-05-30T16:11:53.758Z", + "payments": [] + }, + { + "id": "4f7b2806-cdc4-40de-979e-82573123b1ce", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 162.88, + "customerRate": 11.72, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:45.076Z", + "updatedAt": "2021-05-30T16:05:06.523Z", + "payments": [] + }, + { + "id": "3a652bb3-69b2-4e9b-b6e7-804568a9a76b", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 54.97, + "customerRate": 167.68, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:45.012Z", + "updatedAt": "2021-05-30T16:05:20.251Z", + "payments": [] + } + ] + }, + { + "id": "905654a2-e07d-47a3-b577-c03d100bc94a", + "projectId": 17300, + "userId": "6719d9dc-beca-4731-a4be-a214152ccadf", + "jobId": "fd13ad99-f16a-4362-9274-80f5f38895c3", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 66, + "customerRate": 132.43, + "rateType": "daily", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-19T07:40:44.211Z", + "updatedAt": "2021-05-30T11:48:42.523Z", + "workPeriods": [ + { + "id": "6ca0c12a-eecb-4d12-b40d-89a19f2c38ad", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 216.67, + "customerRate": 281.91, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:43.331Z", + "updatedAt": "2021-05-30T16:11:52.856Z", + "payments": [] + }, + { + "id": "92f9e88b-bc75-4934-9c3f-cdb3a846e545", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 85.45, + "customerRate": 273.89, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:43.339Z", + "updatedAt": "2021-05-30T16:04:18.164Z", + "payments": [] + }, + { + "id": "bc7af7e8-873a-4ddf-a3a5-8b5f0db41827", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": null, + "memberRate": 239.68, + "customerRate": 180.76, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:43.254Z", + "updatedAt": "2021-05-30T16:03:45.211Z", + "payments": [] + }, + { + "id": "11084a75-bae7-4c93-a2fc-44ff691b6ded", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 79.89, + "customerRate": 174.2, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:43.335Z", + "updatedAt": "2021-05-30T16:05:54.898Z", + "payments": [] + }, + { + "id": "6c3a492a-d2bf-4740-91b0-0d877b044268", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 66.85, + "customerRate": 154.65, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:43.275Z", + "updatedAt": "2021-05-30T16:04:41.476Z", + "payments": [] + } + ] + }, + { + "id": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "projectId": 16718, + "userId": "a953dce3-8dd3-413f-b253-0ca76ff59f36", + "jobId": "fb2f5f9b-5874-4dcd-af94-727fc0409760", + "status": "sourcing", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 269.46, + "customerRate": 138.32, + "rateType": "monthly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-26T08:24:56.376Z", + "updatedAt": "2021-05-30T11:48:51.233Z", + "workPeriods": [ + { + "id": "0ed78625-e5f0-4f05-b326-632a002d150a", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 46.62, + "customerRate": 76.96, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:52.006Z", + "updatedAt": "2021-05-30T16:11:58.578Z", + "payments": [] + }, + { + "id": "279989ed-1e9e-45e9-bdb8-26c03d396344", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 173.64, + "customerRate": 160.37, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:51.981Z", + "updatedAt": "2021-05-30T16:05:33.514Z", + "payments": [] + }, + { + "id": "31530732-4c7a-429c-9384-c219f16590fa", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 239.68, + "customerRate": 84.09, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:51.988Z", + "updatedAt": "2021-05-30T16:05:27.123Z", + "payments": [] + }, + { + "id": "91065330-b979-4b3c-b084-0e14b6be6740", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 191.95, + "customerRate": 260.09, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:51.984Z", + "updatedAt": "2021-05-30T16:04:19.946Z", + "payments": [] + }, + { + "id": "06187a37-d29a-4bdb-bcb1-e0e7f57eec4a", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 124.66, + "customerRate": 219.67, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:51.978Z", + "updatedAt": "2021-05-30T16:06:00.996Z", + "payments": [] + } + ] + }, + { + "id": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "projectId": 16870, + "userId": "cc959274-bb53-4612-a4f4-af62496b026c", + "jobId": "fe8da845-5313-496f-b859-9824bd06a0db", + "status": "placed", + "startDate": "2021-01-12", + "endDate": "2021-02-12", + "memberRate": 240.84, + "customerRate": 188.33, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:30:37.276Z", + "updatedAt": "2021-05-30T11:48:54.737Z", + "workPeriods": [ + { + "id": "c44339a8-5562-493d-9e59-02fded34dadd", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 6.22, + "customerRate": 62.03, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:55.482Z", + "updatedAt": "2021-05-30T16:03:38.050Z", + "payments": [] + }, + { + "id": "6e8627d9-f3b9-4e56-ba48-0d4cd0572beb", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 21.58, + "customerRate": 72.49, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:55.537Z", + "updatedAt": "2021-05-30T16:04:39.704Z", + "payments": [] + }, + { + "id": "20b18eeb-a78f-4ff2-8a3b-fbd1cfba567c", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": null, + "memberRate": 41.26, + "customerRate": 286.29, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:55.526Z", + "updatedAt": "2021-05-30T16:05:40.643Z", + "payments": [] + }, + { + "id": "36abc507-0e01-46e3-ab78-52c0e8f848b1", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 102.8, + "customerRate": 149.44, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:55.539Z", + "updatedAt": "2021-05-30T16:05:22.767Z", + "payments": [] + }, + { + "id": "4b8cc238-bb26-4fbf-ab74-a86c1d9a47ce", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 4, + "memberRate": 96.7, + "customerRate": 13.09, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:55.532Z", + "updatedAt": "2021-05-30T16:12:00.505Z", + "payments": [] + } + ] + }, + { + "id": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "projectId": 16870, + "userId": "acdf9ebe-8358-4bd3-9374-1d86cf27e5f4", + "jobId": "fe8da845-5313-496f-b859-9824bd06a0db", + "status": "placed", + "startDate": "2021-01-21", + "endDate": "2021-02-21", + "memberRate": 114.33, + "customerRate": 258.37, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:30:59.306Z", + "updatedAt": "2021-05-30T11:48:56.459Z", + "workPeriods": [ + { + "id": "77dcf745-bd59-40d8-b563-75a4d5354d29", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-02-14", + "endDate": "2021-02-20", + "daysWorked": null, + "memberRate": 158.66, + "customerRate": 19.76, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:57.208Z", + "updatedAt": "2021-05-30T16:04:30.745Z", + "payments": [] + }, + { + "id": "97cb1bad-772a-4f1e-a5a3-f0b19ae766f2", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-02-21", + "endDate": "2021-02-27", + "daysWorked": 0, + "memberRate": 69.43, + "customerRate": 22.82, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:57.210Z", + "updatedAt": "2021-05-30T16:12:01.358Z", + "payments": [] + }, + { + "id": "4c626a59-e591-4e7a-88cb-1d601b9b8493", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 10.15, + "customerRate": 213.97, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:57.302Z", + "updatedAt": "2021-05-30T16:05:10.190Z", + "payments": [] + }, + { + "id": "55b66454-3a29-4163-9d97-7ecd2e805f71", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 2, + "memberRate": 230.66, + "customerRate": 193.93, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:57.225Z", + "updatedAt": "2021-05-30T16:12:01.359Z", + "payments": [] + }, + { + "id": "1fa1f111-6574-47b0-8d12-6832541d496c", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": null, + "memberRate": 104.6, + "customerRate": 62.03, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:57.244Z", + "updatedAt": "2021-05-30T16:05:42.410Z", + "payments": [] + }, + { + "id": "5bc5686b-95b3-49d7-9c8e-50f1dfdcb82e", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 165.75, + "customerRate": 262.91, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:57.306Z", + "updatedAt": "2021-05-30T16:04:57.957Z", + "payments": [] + } + ] + }, + { + "id": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "projectId": 16762, + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "jobId": "fe481d1c-cf87-49c1-9370-695f9f754041", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 61.33, + "customerRate": 84.88, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-12T10:53:32.373Z", + "updatedAt": "2021-05-30T11:48:49.532Z", + "workPeriods": [ + { + "id": "a154b1fb-06d3-4cfd-97d3-0a810a1c4317", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 202.33, + "customerRate": 128, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:50.279Z", + "updatedAt": "2021-05-30T16:04:09.261Z", + "payments": [] + }, + { + "id": "a35207bd-ac1d-4539-b7bb-7a923c8a6f7f", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 294.23, + "customerRate": 142.66, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:50.330Z", + "updatedAt": "2021-05-30T16:11:57.609Z", + "payments": [] + }, + { + "id": "3ad03850-ebe9-4227-8b30-1303b20bbd31", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 157.6, + "customerRate": 40.76, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:50.292Z", + "updatedAt": "2021-05-30T16:05:19.240Z", + "payments": [] + }, + { + "id": "2ea4bffd-2519-422f-8baa-a0f74b3b398b", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 218.59, + "customerRate": 195.92, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:50.340Z", + "updatedAt": "2021-05-30T16:05:30.731Z", + "payments": [] + }, + { + "id": "b34361ca-eb0e-47f7-86d9-3bccbb6839d5", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": null, + "memberRate": 238.31, + "customerRate": 11.09, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:50.288Z", + "updatedAt": "2021-05-30T16:03:55.011Z", + "payments": [] + }, + { + "id": "e1099a6a-7c6b-465d-bb1b-517c3fbd06f1", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 105.58, + "customerRate": 297.59, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:50.297Z", + "updatedAt": "2021-05-30T16:11:57.610Z", + "payments": [] + } + ] + }, + { + "id": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "projectId": 16805, + "userId": "3797d69c-0bf1-421e-b086-81e36ec1f929", + "jobId": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 1.59, + "customerRate": 170.64, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-28T05:15:29.662Z", + "updatedAt": "2021-05-30T11:48:37.381Z", + "workPeriods": [ + { + "id": "33b3b539-5741-49af-a700-fa8e9bd4abba", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 280.11, + "customerRate": 111.64, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:38.136Z", + "updatedAt": "2021-05-30T16:05:25.424Z", + "payments": [] + }, + { + "id": "eca90916-ded1-49d5-8beb-582bba178dd9", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 65.17, + "customerRate": 4.22, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:38.215Z", + "updatedAt": "2021-05-30T16:11:50.205Z", + "payments": [] + }, + { + "id": "2f176676-60f7-4d27-bb79-d1183eb0b7e0", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 196.76, + "customerRate": 42.45, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:38.152Z", + "updatedAt": "2021-05-30T16:05:29.869Z", + "payments": [] + }, + { + "id": "0ac738d8-03be-4ba4-a86b-2a1f65666cd5", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 188.95, + "customerRate": 122.72, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:38.220Z", + "updatedAt": "2021-05-30T16:05:59.284Z", + "payments": [] + }, + { + "id": "b180bd57-30b3-4092-affc-c306401edd7d", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 4, + "memberRate": 126.33, + "customerRate": 160.37, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:38.150Z", + "updatedAt": "2021-05-30T16:03:57.617Z", + "payments": [] + } + ] + }, + { + "id": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "projectId": 16739, + "userId": "3ed9015f-09d8-4173-bfcd-5dcc60c52060", + "jobId": "fc5ba131-566f-46fe-8501-79c593241896", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 61.33, + "customerRate": 114.05, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-17T13:41:56.896Z", + "updatedAt": "2021-05-30T11:48:29.760Z", + "workPeriods": [ + { + "id": "7d495eed-d042-4a96-beed-dc2f2c1054c1", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 245.61, + "customerRate": 158.61, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:30.527Z", + "updatedAt": "2021-05-30T16:04:25.202Z", + "payments": [] + }, + { + "id": "7468173d-6d92-4560-802e-6329ab656754", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 3.94, + "customerRate": 131.83, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:30.544Z", + "updatedAt": "2021-05-30T16:04:36.153Z", + "payments": [] + }, + { + "id": "1dc38edd-3b56-4ed3-ae6c-ea2527076b32", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 43.7, + "customerRate": 154.12, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:30.606Z", + "updatedAt": "2021-05-30T16:05:46.841Z", + "payments": [] + }, + { + "id": "212909c4-b1e9-4d12-b2ca-4175ccbb2d7f", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 85.9, + "customerRate": 134.84, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:30.530Z", + "updatedAt": "2021-05-30T16:11:45.643Z", + "payments": [] + }, + { + "id": "d4dffbb9-2224-4429-9d7e-4bd9d33dba70", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 4.63, + "customerRate": 152.35, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:30.547Z", + "updatedAt": "2021-05-30T16:03:22.384Z", + "payments": [] + }, + { + "id": "b6b60c49-615a-4367-b644-af68485b4293", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 18.46, + "customerRate": 171.14, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:30.540Z", + "updatedAt": "2021-05-30T16:11:45.644Z", + "payments": [] + } + ] + }, + { + "id": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "projectId": 17324, + "userId": "9807980a-a9e4-4f24-a48b-311fcdbf1f47", + "jobId": "fefd2618-9b66-4431-9874-1d02d7a37d90", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 287.14, + "customerRate": 146.2, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-27T05:05:05.125Z", + "updatedAt": "2021-05-30T11:49:06.302Z", + "workPeriods": [ + { + "id": "11d7db8c-b4a9-47b0-b24a-e45d4dc5fae4", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": null, + "memberRate": 85.95, + "customerRate": 178.12, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:07.103Z", + "updatedAt": "2021-05-30T16:05:53.988Z", + "payments": [] + }, + { + "id": "d711870a-4f78-431b-b5b5-ae5157999a0c", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 58.11, + "customerRate": 143.99, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:07.137Z", + "updatedAt": "2021-05-30T16:03:20.574Z", + "payments": [] + }, + { + "id": "d0266458-f9d2-42db-a716-6f114b4a0be0", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 36.75, + "customerRate": 269.78, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:07.095Z", + "updatedAt": "2021-05-30T16:03:25.900Z", + "payments": [] + }, + { + "id": "248de422-69c3-4c5b-8919-ba18113d0350", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 69.67, + "customerRate": 193.93, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:07.106Z", + "updatedAt": "2021-05-30T16:05:36.306Z", + "payments": [] + }, + { + "id": "6aabe458-6e77-4fbd-9092-d811e7bbd21d", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 126.28, + "customerRate": 213.99, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:07.144Z", + "updatedAt": "2021-05-30T16:12:08.558Z", + "payments": [] + }, + { + "id": "662931c7-09a4-43d9-a838-cb275296e818", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 177.96, + "customerRate": 101.95, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:07.100Z", + "updatedAt": "2021-05-30T16:12:08.560Z", + "payments": [] + } + ] + }, + { + "id": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "projectId": 17103, + "userId": "8fe0c1c3-e63e-4047-9854-01f03b166bd8", + "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "status": "closed", + "startDate": "2021-01-02", + "endDate": "2021-02-02", + "memberRate": 85.22, + "customerRate": 170.64, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T10:25:45.827Z", + "updatedAt": "2021-05-30T11:49:17.657Z", + "workPeriods": [ + { + "id": "0466ddf6-83ba-41ee-b299-4abb2b5f8a3b", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 0, + "memberRate": 191.67, + "customerRate": 40.76, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:18.405Z", + "updatedAt": "2021-05-30T16:12:15.134Z", + "payments": [] + }, + { + "id": "bd92f07b-4b57-4486-9101-254578cf32f8", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 282.2, + "customerRate": 177.54, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:18.505Z", + "updatedAt": "2021-05-30T16:03:42.520Z", + "payments": [] + }, + { + "id": "9c976d1a-f395-4889-ac9b-38846a083dcb", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 158.66, + "customerRate": 158.21, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:18.500Z", + "updatedAt": "2021-05-30T16:04:11.932Z", + "payments": [] + }, + { + "id": "62bf7ac9-bea9-4f96-8a28-2a3a8dbbc48f", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 21.58, + "customerRate": 10.21, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:18.439Z", + "updatedAt": "2021-05-30T16:04:50.801Z", + "payments": [] + }, + { + "id": "05fb419d-927c-4264-b346-905ba7a55f49", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 2, + "memberRate": 294.55, + "customerRate": 40.52, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:18.492Z", + "updatedAt": "2021-05-30T16:12:15.133Z", + "payments": [] + }, + { + "id": "c4b535c4-0c6f-4420-930e-0103aea68057", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 4, + "memberRate": 42.79, + "customerRate": 298.27, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:18.490Z", + "updatedAt": "2021-05-30T16:03:36.317Z", + "payments": [] + } + ] + }, + { + "id": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "projectId": 17091, + "userId": "de029f4b-f07b-4f8e-bc58-d928b8d8d289", + "jobId": "fb8b92f6-4ffb-4ba6-8c38-c2d4a151f76b", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 271.93, + "customerRate": 258.37, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:46:05.754Z", + "updatedAt": "2021-05-30T11:49:14.166Z", + "workPeriods": [ + { + "id": "f234f6bb-a90f-4f2d-a205-24ac45f09246", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 273.55, + "customerRate": 245.82, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:14.947Z", + "updatedAt": "2021-05-30T16:02:56.406Z", + "payments": [] + }, + { + "id": "2962c8d7-aeab-422e-aa9e-fd76a2c559d6", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 228.98, + "customerRate": 158.21, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:15.005Z", + "updatedAt": "2021-05-30T16:12:13.268Z", + "payments": [] + }, + { + "id": "729c31fb-dcd7-4b1e-bab8-b47f2db27f12", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 45.72, + "customerRate": 131.63, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:14.943Z", + "updatedAt": "2021-05-30T16:04:37.868Z", + "payments": [] + }, + { + "id": "4dff33bc-ef83-425c-a07d-f49a12e2485f", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 200.17, + "customerRate": 286.15, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:14.956Z", + "updatedAt": "2021-05-30T16:05:07.460Z", + "payments": [] + }, + { + "id": "2b2aaaba-2698-4b32-b6b9-e31e040ee023", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 239.68, + "customerRate": 12.51, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:14.958Z", + "updatedAt": "2021-05-30T16:05:31.643Z", + "payments": [] + } + ] + }, + { + "id": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "projectId": 16718, + "userId": "085fc95d-0336-4572-a641-6c8334e7f0c9", + "jobId": "fb2f5f9b-5874-4dcd-af94-727fc0409760", + "status": "sourcing", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 114.33, + "customerRate": 100.25, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-26T08:27:33.886Z", + "updatedAt": "2021-05-30T11:49:22.899Z", + "workPeriods": [ + { + "id": "ddcfc959-d749-45dc-9e9f-f18a893f9e1a", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 216.18, + "customerRate": 269.78, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:23.644Z", + "updatedAt": "2021-05-30T16:12:17.859Z", + "payments": [] + }, + { + "id": "a322ee7e-7f23-4f9f-b2d8-286b574efd7f", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 115.09, + "customerRate": 107.07, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:23.681Z", + "updatedAt": "2021-05-30T16:04:07.452Z", + "payments": [] + }, + { + "id": "77f9b42c-6e67-4363-8b43-aa0b70a904e1", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 295.92, + "customerRate": 38.47, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:23.734Z", + "updatedAt": "2021-05-30T16:04:29.823Z", + "payments": [] + }, + { + "id": "20fc029b-108a-4f12-aec9-ba36619d4ce7", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 124.66, + "customerRate": 105, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:23.730Z", + "updatedAt": "2021-05-30T16:12:17.860Z", + "payments": [] + }, + { + "id": "662f11e5-c02e-460d-989e-1396ff4f00a6", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 4, + "memberRate": 85.69, + "customerRate": 289.93, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:23.678Z", + "updatedAt": "2021-05-30T16:04:45.827Z", + "payments": [] + }, + { + "id": "f8d56b84-b374-4975-81f8-7fab96463243", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": null, + "memberRate": 230.93, + "customerRate": 99.32, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:23.666Z", + "updatedAt": "2021-05-30T16:02:49.121Z", + "payments": [] + } + ] + }, + { + "id": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "projectId": 16870, + "userId": "46550d28-0f34-4292-908f-02f1a34ac278", + "jobId": "fe539bef-9119-4a8c-b7b0-915e7e3a3ba3", + "status": "placed", + "startDate": "2021-01-13", + "endDate": "2021-02-13", + "memberRate": 85.22, + "customerRate": 265.1, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T08:45:58.009Z", + "updatedAt": "2021-05-30T11:49:12.395Z", + "workPeriods": [ + { + "id": "dae10e27-1bec-4004-adb9-25a09a29f58d", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 3, + "memberRate": 82.71, + "customerRate": 103.9, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:13.147Z", + "updatedAt": "2021-05-30T16:12:12.417Z", + "payments": [] + }, + { + "id": "60cfd6f3-4eed-4e2f-98be-f1377648d700", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 5, + "memberRate": 30.12, + "customerRate": 229.65, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:13.155Z", + "updatedAt": "2021-05-30T16:04:52.604Z", + "payments": [] + }, + { + "id": "5356e3d0-fa3e-4a4a-a94b-3d58745c09f7", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 191.92, + "customerRate": 271.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:13.213Z", + "updatedAt": "2021-05-30T16:05:01.471Z", + "payments": [] + }, + { + "id": "394196b1-7fde-4b3e-a6f2-1d95cd93c27d", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 257.15, + "customerRate": 31.9, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:13.168Z", + "updatedAt": "2021-05-30T16:05:21.118Z", + "payments": [] + }, + { + "id": "a6fa8266-f335-4148-96a7-3f63dc66aec4", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 4, + "memberRate": 159.75, + "customerRate": 168.78, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:13.142Z", + "updatedAt": "2021-05-30T16:04:03.881Z", + "payments": [] + } + ] + }, + { + "id": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "projectId": 17103, + "userId": "9e4b1242-9b14-4159-bd0b-de7fa1803ca9", + "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "status": "cancelled", + "startDate": "2021-01-21", + "endDate": "2021-02-21", + "memberRate": 61.33, + "customerRate": 196.21, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T10:25:50.725Z", + "updatedAt": "2021-05-30T11:49:19.410Z", + "workPeriods": [ + { + "id": "217f124d-37db-49a8-9cac-187c5c8b2905", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 157.6, + "customerRate": 211.33, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:20.202Z", + "updatedAt": "2021-05-30T16:05:38.038Z", + "payments": [] + }, + { + "id": "b826c9b8-12f5-4567-bd62-df524bb690a2", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 26.55, + "customerRate": 24.64, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:20.194Z", + "updatedAt": "2021-05-30T16:03:47.803Z", + "payments": [] + }, + { + "id": "3e6436c6-f6d4-4b33-8027-1269b167554f", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 174.63, + "customerRate": 234.94, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:20.200Z", + "updatedAt": "2021-05-30T16:05:18.330Z", + "payments": [] + }, + { + "id": "56f49073-e651-479c-967c-8ba58e36b8e6", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 2, + "memberRate": 166.58, + "customerRate": 107.72, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:20.236Z", + "updatedAt": "2021-05-30T16:12:15.994Z", + "payments": [] + }, + { + "id": "f04d5bb8-abba-4447-92f0-005e823238f8", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-02-21", + "endDate": "2021-02-27", + "daysWorked": 0, + "memberRate": 143.95, + "customerRate": 249.53, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:20.193Z", + "updatedAt": "2021-05-30T16:12:15.996Z", + "payments": [] + }, + { + "id": "fe874682-2ba6-4f42-929b-efc9e05adafd", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-02-14", + "endDate": "2021-02-20", + "daysWorked": 5, + "memberRate": 186.09, + "customerRate": 217.18, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:20.197Z", + "updatedAt": "2021-05-30T15:58:40.816Z", + "payments": [] + } + ] + }, + { + "id": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "projectId": 17324, + "userId": "4709473d-f060-4102-87f8-4d51ff0b34c1", + "jobId": "fefd2618-9b66-4431-9874-1d02d7a37d90", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 271.93, + "customerRate": 188.33, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-20T06:52:40.679Z", + "updatedAt": "2021-05-30T11:49:15.876Z", + "workPeriods": [ + { + "id": "f28bd617-dce3-47c0-a9ab-6b2ff321d206", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 185.99, + "customerRate": 213.97, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:16.700Z", + "updatedAt": "2021-05-30T16:12:14.202Z", + "payments": [] + }, + { + "id": "c447a850-2549-4c6a-ad3e-47cb6b26ac0b", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "memberRate": 22.66, + "customerRate": 215.7, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:16.706Z", + "updatedAt": "2021-05-30T16:03:37.121Z", + "payments": [] + }, + { + "id": "66732a8f-7bab-4e46-8eda-c58f28344114", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 82.71, + "customerRate": 24.46, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:16.759Z", + "updatedAt": "2021-05-30T16:04:44.983Z", + "payments": [] + }, + { + "id": "444fbe9a-616e-443a-a1b5-aadfe7c617ff", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 230.09, + "customerRate": 248.77, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:16.687Z", + "updatedAt": "2021-05-30T16:05:13.852Z", + "payments": [] + }, + { + "id": "50865971-2fd2-4576-a799-8ab438e9dd75", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 4, + "memberRate": 10.25, + "customerRate": 159.65, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:16.701Z", + "updatedAt": "2021-05-30T16:05:05.541Z", + "payments": [] + } + ] + }, + { + "id": "1ad758ab-c19f-4247-954a-4581420aba8a", + "projectId": 17363, + "userId": "dbf68f12-69a4-4592-a0ab-cf68d9ed7ae4", + "jobId": "fd48d96e-b0f2-43b7-8a48-f4fa194d6bc8", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 114.33, + "customerRate": 217.99, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-27T11:25:35.292Z", + "updatedAt": "2021-05-30T11:49:08.019Z", + "workPeriods": [ + { + "id": "c4c8588e-68a4-4b82-be91-a3d98661ffba", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 54.99, + "customerRate": 109.55, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:08.879Z", + "updatedAt": "2021-05-30T16:03:35.487Z", + "payments": [] + }, + { + "id": "83cb4174-6ee3-4557-97c1-120c46054af6", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": null, + "memberRate": 213.59, + "customerRate": 176.11, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:08.882Z", + "updatedAt": "2021-05-30T16:04:22.560Z", + "payments": [] + }, + { + "id": "8ff2339f-d90a-4ac2-9798-3158d0746d53", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 298.88, + "customerRate": 235.7, + "paymentStatus": "cancelled", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:08.875Z", + "updatedAt": "2021-05-30T16:12:09.701Z", + "payments": [] + }, + { + "id": "5ecaa40c-1fb3-4df7-9870-6fc3c2bc1bca", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 114.59, + "customerRate": 203.92, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:08.877Z", + "updatedAt": "2021-05-30T16:04:53.463Z", + "payments": [] + }, + { + "id": "38afaa09-32da-4d81-b2f5-0c5e31af617f", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 176.02, + "customerRate": 47.87, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:08.867Z", + "updatedAt": "2021-05-30T16:05:21.960Z", + "payments": [] + } + ] + }, + { + "id": "198fb1a7-f662-4e35-aa8b-7dd171d2f519", + "projectId": 17300, + "userId": "2bba34d5-20e4-46d6-bfc1-05736b17afbb", + "jobId": "fd13ad99-f16a-4362-9274-80f5f38895c3", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 104.85, + "customerRate": 138.32, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-20T06:52:10.333Z", + "updatedAt": "2021-05-30T11:49:09.704Z", + "workPeriods": [ + { + "id": "cdda5ed7-5ddf-4985-8856-9b691c196db3", + "resourceBookingId": "198fb1a7-f662-4e35-aa8b-7dd171d2f519", + "userHandle": "GunaK-TopCoder", + "projectId": 17300, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "memberRate": 204.06, + "customerRate": 96.56, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:10.560Z", + "updatedAt": "2021-05-30T16:12:10.667Z", + "payments": [] + }, + { + "id": "ee556bae-58ad-4f64-a48a-ce87362bad3d", + "resourceBookingId": "198fb1a7-f662-4e35-aa8b-7dd171d2f519", + "userHandle": "GunaK-TopCoder", + "projectId": 17300, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "memberRate": 46.62, + "customerRate": 280.07, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:10.570Z", + "updatedAt": "2021-05-30T16:12:10.668Z", + "payments": [] + }, + { + "id": "52c34f5f-290c-4ff0-9d7a-30f43868f83d", + "resourceBookingId": "198fb1a7-f662-4e35-aa8b-7dd171d2f519", + "userHandle": "GunaK-TopCoder", + "projectId": 17300, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "memberRate": 108.35, + "customerRate": 298.27, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:10.568Z", + "updatedAt": "2021-05-30T16:05:03.441Z", + "payments": [] + }, + { + "id": "bd27f1a4-b7ee-4526-a21c-fd8c4955fe5e", + "resourceBookingId": "198fb1a7-f662-4e35-aa8b-7dd171d2f519", + "userHandle": "GunaK-TopCoder", + "projectId": 17300, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 18.57, + "customerRate": 272.37, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:10.566Z", + "updatedAt": "2021-05-30T16:03:43.365Z", + "payments": [] + }, + { + "id": "64c0b0b8-9c77-4e7c-9d0a-14541e8e0a34", + "resourceBookingId": "198fb1a7-f662-4e35-aa8b-7dd171d2f519", + "userHandle": "GunaK-TopCoder", + "projectId": 17300, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "memberRate": 266.82, + "customerRate": 268.61, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:10.622Z", + "updatedAt": "2021-05-30T16:04:49.270Z", + "payments": [] + }, + { + "id": "f41169e6-e9e0-44ce-a54d-5a84a32085c6", + "resourceBookingId": "198fb1a7-f662-4e35-aa8b-7dd171d2f519", + "userHandle": "GunaK-TopCoder", + "projectId": 17300, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "memberRate": 296.93, + "customerRate": 255.03, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:10.558Z", + "updatedAt": "2021-05-30T16:02:52.797Z", + "payments": [] + } + ] + }, + { + "id": "07f73049-e51a-4394-b61f-b75418afa908", + "projectId": 16739, + "userId": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "jobId": "fc5ba131-566f-46fe-8501-79c593241896", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 66, + "customerRate": 114.05, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-12T10:59:20.006Z", + "updatedAt": "2021-05-30T11:49:21.188Z", + "workPeriods": [ + { + "id": "d99a524f-c8a4-4d46-a42c-dbcddd65b6db", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "memberRate": 214.14, + "customerRate": 212.49, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:22.017Z", + "updatedAt": "2021-05-30T16:03:19.706Z", + "payments": [] + }, + { + "id": "5c4bb82b-e617-4c91-861f-5e0825d43c53", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "memberRate": 70.84, + "customerRate": 136.39, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:21.977Z", + "updatedAt": "2021-05-30T16:04:55.294Z", + "payments": [] + }, + { + "id": "7ff4804e-2a65-4f8b-af5b-24b58c066fd4", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "memberRate": 25.99, + "customerRate": 122.72, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:22.068Z", + "updatedAt": "2021-05-30T16:12:16.993Z", + "payments": [] + }, + { + "id": "da043aba-161e-4894-a3d5-d63678ac89b0", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": null, + "memberRate": 71.99, + "customerRate": 155.48, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:21.974Z", + "updatedAt": "2021-05-30T16:03:18.831Z", + "payments": [] + }, + { + "id": "410e034d-ee48-4a18-aa65-679ef7efcb80", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 4, + "memberRate": 146.28, + "customerRate": 21.33, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:21.961Z", + "updatedAt": "2021-05-30T16:05:16.466Z", + "payments": [] + } + ] + }, + { + "id": "d3cd14c8-9ae8-446a-b554-69240c93a20e", + "projectId": 17091, + "userId": "3e654566-0d6b-404a-a000-c8640252c0e1", + "jobId": "fb8b92f6-4ffb-4ba6-8c38-c2d4a151f76b", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 201.77, + "customerRate": 296.66, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:58:18.513Z", + "updatedAt": "2021-05-30T11:48:25.595Z", + "workPeriods": [] + }, + { + "id": "7d81f99f-e4d2-49ff-8ce5-39832ea972fe", + "projectId": 16870, + "userId": "74219092-52ee-4434-a35e-25f000369645", + "jobId": "fe8da845-5313-496f-b859-9824bd06a0db", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 211.1, + "customerRate": 100.25, + "rateType": "monthly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:30:42.397Z", + "updatedAt": "2021-05-30T11:48:48.669Z", + "workPeriods": [] + }, + { + "id": "e7d96c52-a7ec-40e1-be64-6ef21fab4a1e", + "projectId": 16718, + "userId": "d82d4d41-1f25-4faf-ac24-3b5f8a138fac", + "jobId": "fb2f5f9b-5874-4dcd-af94-727fc0409760", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 271.93, + "customerRate": 58.22, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-17T13:44:23.792Z", + "updatedAt": "2021-05-30T11:48:19.456Z", + "workPeriods": [] + }, + { + "id": "5c3536b3-7523-4706-9396-eaa44ec608bb", + "projectId": 17103, + "userId": "93814e61-3b44-40bf-acaf-477857f52f90", + "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 115.29, + "customerRate": 114.05, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:53:01.882Z", + "updatedAt": "2021-05-30T11:48:58.248Z", + "workPeriods": [] + }, + { + "id": "1553e801-61c1-4069-ac58-bcb206ac2e44", + "projectId": 16781, + "userId": "bea5b4c1-922d-4800-ae27-c2f45b7e20bc", + "jobId": "ff3feeae-d4f7-457c-bff7-215be5efe2b8", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 186.53, + "customerRate": 265.1, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-19T10:07:50.071Z", + "updatedAt": "2021-05-30T11:49:11.520Z", + "workPeriods": [] + }, + { + "id": "a386f79d-724e-4c13-bf85-b8e5a0394617", + "projectId": 16706, + "userId": "f21455ae-a5f1-41a7-86eb-152e0e113b6e", + "jobId": "fc2b006d-997b-49c3-a414-59ee54a48f9f", + "status": "sourcing", + "startDate": null, + "endDate": null, + "memberRate": 115.29, + "customerRate": 146.2, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-26T08:25:01.121Z", + "updatedAt": "2021-05-30T11:39:24.762Z", + "workPeriods": [] + }, + { + "id": "4141d57c-2712-4dab-8140-904ab4364e98", + "projectId": 16714, + "userId": "fc04aa5d-9c34-4dd6-be8d-10ec0e3e6d01", + "jobId": "fc0240f0-8c8f-40ce-a551-e83b45673098", + "status": "sourcing", + "startDate": null, + "endDate": null, + "memberRate": 60.63, + "customerRate": 196.21, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-25T13:12:14.707Z", + "updatedAt": "2021-05-30T11:49:03.637Z", + "workPeriods": [] + }, + { + "id": "042d8158-3cee-4289-839e-1f2a73af1859", + "projectId": 16805, + "userId": "388f9618-5a2c-4a58-b587-3b3dfbb3f584", + "jobId": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 115.29, + "customerRate": 111.21, + "rateType": "monthly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-04-20T08:04:17.892Z", + "updatedAt": "2021-05-30T11:49:24.627Z", + "workPeriods": [] + }, + { + "id": "21ae6f7f-f594-496a-9d87-175fd5820286", + "projectId": 16870, + "userId": "2bba34d5-20e4-46d6-bfc1-05736b17afbb", + "jobId": "fed687e1-4257-48bb-806c-38712f9bf14f", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 30.29, + "customerRate": 217.99, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-04-20T08:04:14.718Z", + "updatedAt": "2021-05-30T11:49:05.373Z", + "workPeriods": [] + }, + { + "id": "41fb1035-3165-4ff9-a2df-62c700fb8b37", + "projectId": 17290, + "userId": "b5bc4d91-2396-467b-8f7d-9a56ffb0feb0", + "jobId": "fe600350-0a6d-4dac-922f-a6a7d285daa1", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 240.84, + "customerRate": 146.2, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-18T04:01:38.141Z", + "updatedAt": "2021-05-30T11:49:02.754Z", + "workPeriods": [] + }, + { + "id": "f0738f2a-c837-42e3-acdf-0e1324d77e53", + "projectId": 16739, + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "jobId": "fc5ba131-566f-46fe-8501-79c593241896", + "status": "placed", + "startDate": null, + "endDate": null, + "memberRate": 271.93, + "customerRate": 258.37, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-17T13:43:44.675Z", + "updatedAt": "2021-05-30T11:48:16.870Z", + "workPeriods": [] } ] -} +} \ No newline at end of file diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 74155753..487bf3f9 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", + "_postman_id": "7f931491-50de-42cf-9e15-24d7a6675667", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -8016,6 +8016,2058 @@ } ] }, + { + "name": "Extended Search Scenarios", + "item": [ + { + "name": "search RB sortBy 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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=id&sortOrder=asc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "id" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id", + "disabled": true + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy status", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=status&sortOrder=asc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "status" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,status", + "disabled": true + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods.id,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods.id,workPeriods.startDate", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods.id,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods.id,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,status,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy rateType", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=rateType&sortOrder=asc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "rateType" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType", + "disabled": true + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods.id,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods.id,workPeriods.startDate", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods.id,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods.id,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,rateType,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy startDate", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=10&sortBy=startDate&sortOrder=asc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "10" + }, + { + "key": "sortBy", + "value": "startDate" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate", + "disabled": true + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods.id,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods.id,workPeriods.startDate", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods.id,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods.id,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,startDate,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy endDate", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=10&sortBy=endDate&sortOrder=asc&fields=id,endDate,workPeriods", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "10" + }, + { + "key": "sortBy", + "value": "endDate" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate", + "disabled": true + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods.id,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods.id,workPeriods.startDate", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods.id,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods.id,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,endDate,workPeriods" + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy customerRate", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=customerRate&sortOrder=asc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "customerRate" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate", + "disabled": true + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods.id,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods.id,workPeriods.startDate", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods.id,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods.id,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,customerRate,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy memberRate", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=memberRate&sortOrder=asc", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "memberRate" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate", + "disabled": true + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-11", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods.id,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods.id,workPeriods.startDate", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods.id,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods.id,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,memberRate,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy workPeriods.userHandle", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=10&sortBy=workPeriods.userHandle&workPeriods.startDate=2021-01-03&sortOrder=asc&fields=id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "10" + }, + { + "key": "sortBy", + "value": "workPeriods.userHandle" + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle" + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy workPeriods.daysWorked", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=10&sortBy=workPeriods.daysWorked&workPeriods.startDate=2021-01-10&sortOrder=asc&fields=id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "10" + }, + { + "key": "sortBy", + "value": "workPeriods.daysWorked" + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-10" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked" + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-16", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysWorked,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy workPeriods.customerRate", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=workPeriods.customerRate&workPeriods.startDate=2021-01-03&sortOrder=asc&fields=id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "workPeriods.customerRate" + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate" + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy workPeriods.memberRate", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=workPeriods.memberRate&workPeriods.startDate=2021-01-03&sortOrder=asc&fields=id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "workPeriods.memberRate" + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate" + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "search RB sortBy workPeriods.paymentStatus", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=workPeriods.paymentStatus&workPeriods.startDate=2021-01-03&sortOrder=asc&fields=id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "5" + }, + { + "key": "sortBy", + "value": "workPeriods.paymentStatus" + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus" + }, + { + "key": "status", + "value": "placed", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,status", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-01-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,startDate", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-02-01", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,endDate", + "disabled": true + }, + { + "key": "rateType", + "value": "weekly", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,rateType", + "disabled": true + }, + { + "key": "jobId", + "value": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,jobId", + "disabled": true + }, + { + "key": "projectId", + "value": "16870", + "disabled": true + }, + { + "key": "projectIds", + "value": "16870,16805,16739,17091", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,projectId", + "disabled": true + }, + { + "key": "workPeriods.paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,workPeriods.paymentStatus", + "disabled": true + }, + { + "key": "workPeriods.endDate", + "value": "2021-01-09", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,workPeriods.endDate", + "disabled": true + }, + { + "key": "workPeriods.userHandle", + "value": "GunaK-TopCoder", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentStatus,workPeriods.userHandle", + "disabled": true + }, + { + "key": "fields", + "value": "id,workPeriods", + "disabled": true + } + ] + } + }, + "response": [] + } + ] + }, { "name": "create resource booking with booking manager", "event": [ @@ -27007,4 +29059,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/topcoder-bookings.postman_environment.json b/docs/topcoder-bookings.postman_environment.json index 837b55db..12611873 100644 --- a/docs/topcoder-bookings.postman_environment.json +++ b/docs/topcoder-bookings.postman_environment.json @@ -1,5 +1,5 @@ { - "id": "228f4dcc-6914-462e-9b56-3285b643a2f8", + "id": "d4a3eeb9-b0f8-4dca-ae1a-78bc6e43bcce", "name": "topcoder-bookings", "values": [ { @@ -142,51 +142,6 @@ "value": "17234", "enabled": true }, - { - "key": "jobId", - "value": "", - "enabled": true - }, - { - "key": "jobIdCreatedByM2M", - "value": "", - "enabled": true - }, - { - "key": "jobIdCreatedByMember", - "value": "", - "enabled": true - }, - { - "key": "jobCandidateId", - "value": "", - "enabled": true - }, - { - "key": "jobCandidateIdCreatedByM2M", - "value": "", - "enabled": true - }, - { - "key": "resourceBookingId", - "value": "", - "enabled": true - }, - { - "key": "resourceBookingIdCreatedByM2M", - "value": "", - "enabled": true - }, - { - "key": "workPeriodId", - "value": "", - "enabled": true - }, - { - "key": "workPeriodIdCreatedByM2M", - "value": "", - "enabled": true - }, { "key": "token_m2m_create_work_period", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6dGFhcy13b3JrUGVyaW9kcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.tgKxTrlI8bu6CVFk4-kFB1gKHL-L6X8akKYYREjZiSE", @@ -242,106 +197,6 @@ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJhbGw6dGFhcy13b3JrUGVyaW9kUGF5bWVudHMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.ut5vdW-124nBshaeGBvg4mCue4XlRljWlV7OneJk4i4", "enabled": true }, - { - "key": "job_id_created_by_administrator", - "value": "", - "enabled": true - }, - { - "key": "resource_bookings_id_created_by_administrator", - "value": "", - "enabled": true - }, - { - "key": "workPeriodId_created_by_administrator", - "value": "", - "enabled": true - }, - { - "key": "job_id_created_by_member", - "value": "", - "enabled": true - }, - { - "key": "resource_booking_id_created_by_member", - "value": "", - "enabled": true - }, - { - "key": "resource_booking_id_created_for_member", - "value": "", - "enabled": true - }, - { - "key": "workPeriodId_created_for_member", - "value": "", - "enabled": true - }, - { - "key": "resource_booking_id_created_for_connect_manager", - "value": "", - "enabled": true - }, - { - "key": "job_id_created_by_connect_manager", - "value": "", - "enabled": true - }, - { - "key": "workPeriodId_created_for_connect_manager", - "value": "", - "enabled": true - }, - { - "key": "job_candidate_id_created_by_administrator", - "value": "", - "enabled": true - }, - { - "key": "workPeriodPaymentId", - "value": "", - "enabled": true - }, - { - "key": "workPeriodPaymentIdCreatedByM2M", - "value": "", - "enabled": true - }, - { - "key": "workPeriodPaymentId_created_by_administrator", - "value": "", - "enabled": true - }, - { - "key": "job_id_created_for_member", - "value": "", - "enabled": true - }, - { - "key": "resource_bookings_id_created_for_member", - "value": "", - "enabled": true - }, - { - "key": "workPeriodPaymentId_created_for_member", - "value": "", - "enabled": true - }, - { - "key": "job_id_created_for_connect_manager", - "value": "", - "enabled": true - }, - { - "key": "resource_bookings_id_created_for_connect_manager", - "value": "", - "enabled": true - }, - { - "key": "workPeriodPaymentId_created_for_connect_manager", - "value": "", - "enabled": true - }, { "key": "interviewRound", "value": "1", @@ -352,46 +207,16 @@ "value": "1", "enabled": true }, - { - "key": "job_id_created_for_member", - "value": "", - "enabled": true - }, - { - "key": "job_candidate_id_created_for_member", - "value": "", - "enabled": true - }, { "key": "interview_round_created_for_member", "value": "1", "enabled": true }, - { - "key": "job_id_created_for_connect_manager", - "value": "", - "enabled": true - }, - { - "key": "job_candidate_id_created_for_connect_manager", - "value": "", - "enabled": true - }, { "key": "interview_round_created_for_connect_manager", "value": "1", "enabled": true }, - { - "key": "completedInterviewJobCandidateId", - "value": "", - "enabled": true - }, - { - "key": "completedInterviewRound", - "value": "", - "enabled": true - }, { "key": "token_m2m_create_interviews", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6dGFhcy1pbnRlcnZpZXdzIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.VD5j8qdgK3ZPctqD-Nb5KKfSeFIuyajc7Q-wQ_kabIk", @@ -421,49 +246,9 @@ "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-05-10T05:06:38.661Z", - "_postman_exported_using": "Postman/8.3.1" + "_postman_exported_at": "2021-05-30T20:07:50.354Z", + "_postman_exported_using": "Postman/8.5.1" } \ No newline at end of file diff --git a/src/common/helper.js b/src/common/helper.js index e68e5c58..afa9d89f 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -2,50 +2,50 @@ * This file defines helper methods */ -const fs = require('fs'); -const querystring = require('querystring'); -const Confirm = require('prompt-confirm'); -const Bottleneck = require('bottleneck'); -const AWS = require('aws-sdk'); -const config = require('config'); -const HttpStatus = require('http-status-codes'); -const _ = require('lodash'); -const request = require('superagent'); -const elasticsearch = require('@elastic/elasticsearch'); +const fs = require('fs') +const querystring = require('querystring') +const Confirm = require('prompt-confirm') +const Bottleneck = require('bottleneck') +const AWS = require('aws-sdk') +const config = require('config') +const HttpStatus = require('http-status-codes') +const _ = require('lodash') +const request = require('superagent') +const elasticsearch = require('@elastic/elasticsearch') const { - ResponseError: ESResponseError, -} = require('@elastic/elasticsearch/lib/errors'); -const errors = require('../common/errors'); -const logger = require('./logger'); -const models = require('../models'); -const eventDispatcher = require('./eventDispatcher'); -const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper'); -const moment = require('moment'); + ResponseError: ESResponseError +} = require('@elastic/elasticsearch/lib/errors') +const errors = require('../common/errors') +const logger = require('./logger') +const models = require('../models') +const eventDispatcher = require('./eventDispatcher') +const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') +const moment = require('moment') const localLogger = { debug: (message) => logger.debug({ component: 'helper', context: message.context, - message: message.message, + message: message.message }), error: (message) => logger.error({ component: 'helper', context: message.context, - message: message.message, + message: message.message }), info: (message) => logger.info({ component: 'helper', context: message.context, - message: message.message, - }), -}; + message: message.message + }) +} -AWS.config.region = config.esConfig.AWS_REGION; +AWS.config.region = config.esConfig.AWS_REGION -const m2mAuth = require('tc-core-library-js').auth.m2m; +const m2mAuth = require('tc-core-library-js').auth.m2m const m2m = m2mAuth( _.pick(config, [ @@ -53,9 +53,9 @@ const m2m = m2mAuth( 'AUTH0_AUDIENCE', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', - 'AUTH0_PROXY_SERVER_URL', + 'AUTH0_PROXY_SERVER_URL' ]) -); +) const m2mForUbahn = m2mAuth({ AUTH0_AUDIENCE: config.AUTH0_AUDIENCE_UBAHN, @@ -64,20 +64,20 @@ const m2mForUbahn = m2mAuth({ 'TOKEN_CACHE_TIME', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', - 'AUTH0_PROXY_SERVER_URL', - ]), -}); + 'AUTH0_PROXY_SERVER_URL' + ]) +}) -let busApiClient; +let busApiClient /** * Get bus api client. * * @returns {Object} the bus api client */ -function getBusApiClient() { +function getBusApiClient () { if (busApiClient) { - return busApiClient; + return busApiClient } busApiClient = busApi( _.pick(config, [ @@ -88,17 +88,17 @@ function getBusApiClient() { 'AUTH0_CLIENT_SECRET', 'BUSAPI_URL', 'KAFKA_ERROR_TOPIC', - 'AUTH0_PROXY_SERVER_URL', + 'AUTH0_PROXY_SERVER_URL' ]) - ); - return busApiClient; + ) + return busApiClient } // ES Client mapping -const esClients = {}; +const esClients = {} // The es index property mapping -const esIndexPropertyMapping = {}; +const esIndexPropertyMapping = {} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { projectId: { type: 'integer' }, externalId: { type: 'keyword' }, @@ -116,8 +116,8 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { jobId: { type: 'keyword' }, userId: { type: 'keyword' }, @@ -150,14 +150,14 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, updatedBy: { type: 'keyword' }, - deletedAt: { type: 'date' }, - }, + deletedAt: { type: 'date' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { projectId: { type: 'integer' }, userId: { type: 'keyword' }, @@ -174,7 +174,10 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { properties: { id: { type: 'keyword' }, resourceBookingId: { type: 'keyword' }, - userHandle: { type: 'keyword' }, + userHandle: { + type: 'keyword', + normalizer: 'lowercaseNormalizer' + }, projectId: { type: 'integer' }, userId: { type: 'keyword' }, startDate: { type: 'date', format: 'yyyy-MM-dd' }, @@ -195,32 +198,32 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, - }, + updatedBy: { type: 'keyword' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, - }, + updatedBy: { type: 'keyword' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} /** * Get the first parameter from cli arguments */ -function getParamFromCliArgs() { - const filteredArgs = process.argv.filter((arg) => !arg.includes('--')); +function getParamFromCliArgs () { + const filteredArgs = process.argv.filter((arg) => !arg.includes('--')) if (filteredArgs.length > 2) { - return filteredArgs[2]; + return filteredArgs[2] } - return null; + return null } /** @@ -228,18 +231,18 @@ function getParamFromCliArgs() { * @param {string} promptQuery the query to ask the user * @param {function} cb the callback function */ -async function promptUser(promptQuery, cb) { +async function promptUser (promptQuery, cb) { if (process.argv.includes('--force')) { - await cb(); - return; + await cb() + return } - const prompt = new Confirm(promptQuery); + const prompt = new Confirm(promptQuery) prompt.ask(async (answer) => { if (answer) { - await cb(); + await cb() } - }); + }) } /** @@ -248,23 +251,38 @@ async function promptUser(promptQuery, cb) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function createIndex(index, logger, esClient = null) { +async function createIndex (index, logger, esClient = null) { if (!esClient) { - esClient = getESClient(); + esClient = getESClient() } - await esClient.indices.create({ + await esClient.indices.create({ index }) + await esClient.indices.close({ index }) + await esClient.indices.putSettings({ + index: index, + body: { + settings: { + analysis: { + normalizer: { + lowercaseNormalizer: { + filter: ['lowercase'] + } + } + } + } + } + }) + await esClient.indices.open({ index }) + await esClient.indices.putMapping({ index, body: { - mappings: { - properties: esIndexPropertyMapping[index], - }, - }, - }); + properties: esIndexPropertyMapping[index] + } + }) logger.info({ component: 'createIndex', - message: `ES Index ${index} creation succeeded!`, - }); + message: `ES Index ${index} creation succeeded!` + }) } /** @@ -273,45 +291,45 @@ async function createIndex(index, logger, esClient = null) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function deleteIndex(index, logger, esClient = null) { +async function deleteIndex (index, logger, esClient = null) { if (!esClient) { - esClient = getESClient(); + esClient = getESClient() } - await esClient.indices.delete({ index }); + await esClient.indices.delete({ index }) logger.info({ component: 'deleteIndex', - message: `ES Index ${index} deletion succeeded!`, - }); + message: `ES Index ${index} deletion succeeded!` + }) } /** * Split data into bulks * @param {Array} data the array of data to split */ -function getBulksFromDocuments(data) { - const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6; - const bulks = []; - let documentIndex = 0; - let currentBulkSize = 0; - let currentBulk = []; +function getBulksFromDocuments (data) { + const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6 + const bulks = [] + let documentIndex = 0 + let currentBulkSize = 0 + let currentBulk = [] while (true) { // break loop when parsed all documents if (documentIndex >= data.length) { - bulks.push(currentBulk); - break; + bulks.push(currentBulk) + break } // check if current document size is greater than the max bulk size, if so, throw error const currentDocumentSize = Buffer.byteLength( JSON.stringify(data[documentIndex]), 'utf-8' - ); + ) if (maxBytes < currentDocumentSize) { throw new Error( `Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.` - ); + ) } if ( @@ -320,17 +338,17 @@ function getBulksFromDocuments(data) { ) { // if adding the current document goes over the max bulk size OR goes over max number of docs // then push the current bulk to bulks array and reset the current bulk - bulks.push(currentBulk); - currentBulk = []; - currentBulkSize = 0; + bulks.push(currentBulk) + currentBulk = [] + currentBulkSize = 0 } else { // otherwise, add document to current bulk - currentBulk.push(data[documentIndex]); - currentBulkSize += currentDocumentSize; - documentIndex++; + currentBulk.push(data[documentIndex]) + currentBulkSize += currentDocumentSize + documentIndex++ } } - return bulks; + return bulks } /** @@ -339,57 +357,57 @@ function getBulksFromDocuments(data) { * @param {Object} indexName the index name * @param {Object} logger the logger object */ -async function indexBulkDataToES(modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); +async function indexBulkDataToES (modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) logger.info({ component: 'indexBulkDataToES', - message: `Reindexing of ${modelName}s started!`, - }); + message: `Reindexing of ${modelName}s started!` + }) - const esClient = getESClient(); + const esClient = getESClient() // clear index - const indexExistsRes = await esClient.indices.exists({ index: indexName }); + const indexExistsRes = await esClient.indices.exists({ index: indexName }) if (indexExistsRes.statusCode !== 404) { - await deleteIndex(indexName, logger, esClient); + await deleteIndex(indexName, logger, esClient) } - await createIndex(indexName, logger, esClient); + await createIndex(indexName, logger, esClient) // get data from db logger.info({ component: 'indexBulkDataToES', - message: 'Getting data from database', - }); - const model = models[modelName]; - const data = await model.findAll({ include }); - const rawObjects = _.map(data, (r) => r.toJSON()); + message: 'Getting data from database' + }) + const model = models[modelName] + const data = await model.findAll({ include }) + const rawObjects = _.map(data, (r) => r.toJSON()) if (_.isEmpty(rawObjects)) { logger.info({ component: 'indexBulkDataToES', - message: `No data in database for ${modelName}`, - }); - return; + message: `No data in database for ${modelName}` + }) + return } - const bulks = getBulksFromDocuments(rawObjects); + const bulks = getBulksFromDocuments(rawObjects) - const startTime = Date.now(); - let doneCount = 0; + const startTime = Date.now() + let doneCount = 0 for (const bulk of bulks) { // send bulk to esclient const body = bulk.flatMap((doc) => [ { index: { _index: indexName, _id: doc.id } }, - doc, - ]); - await esClient.bulk({ refresh: true, body }); - doneCount += bulk.length; + doc + ]) + await esClient.bulk({ refresh: true, body }) + doneCount += bulk.length // log metrics - const timeSpent = Date.now() - startTime; - const avgTimePerDocument = timeSpent / doneCount; - const estimatedLength = avgTimePerDocument * data.length; - const timeLeft = startTime + estimatedLength - Date.now(); + const timeSpent = Date.now() - startTime + const avgTimePerDocument = timeSpent / doneCount + const estimatedLength = avgTimePerDocument * data.length + const timeLeft = startTime + estimatedLength - Date.now() logger.info({ component: 'indexBulkDataToES', message: `Processed ${doneCount} of ${ @@ -398,8 +416,8 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { avgTimePerDocument )}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime( timeLeft - )}`, - }); + )}` + }) } } @@ -410,36 +428,36 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { * @param {string} id the job id * @param {Object} logger the logger object */ -async function indexDataToEsById(id, modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); +async function indexDataToEsById (id, modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) logger.info({ component: 'indexDataToEsById', - message: `Reindexing of ${modelName} with id ${id} started!`, - }); - const esClient = getESClient(); + message: `Reindexing of ${modelName} with id ${id} started!` + }) + const esClient = getESClient() logger.info({ component: 'indexDataToEsById', - message: 'Getting data from database', - }); - const model = models[modelName]; + message: 'Getting data from database' + }) + const model = models[modelName] - const data = await model.findById(id, include); + const data = await model.findById(id, include) logger.info({ component: 'indexDataToEsById', - message: 'Indexing data into Elasticsearch', - }); + message: 'Indexing data into Elasticsearch' + }) await esClient.index({ index: indexName, id: id, - body: data.dataValues, - }); + body: data.dataValues + }) logger.info({ component: 'indexDataToEsById', - message: 'Indexing complete!', - }); + message: 'Indexing complete!' + }) } /** @@ -448,68 +466,68 @@ async function indexDataToEsById(id, modelOpts, indexName, logger) { * @param {Array} dataModels the data models to import * @param {Object} logger the logger object */ -async function importData(pathToFile, dataModels, logger) { +async function importData (pathToFile, dataModels, logger) { // check if file exists if (!fs.existsSync(pathToFile)) { - throw new Error(`File with path ${pathToFile} does not exist`); + throw new Error(`File with path ${pathToFile} does not exist`) } // clear database - logger.info({ component: 'importData', message: 'Clearing database...' }); - await models.sequelize.sync({ force: true }); + logger.info({ component: 'importData', message: 'Clearing database...' }) + await models.sequelize.sync({ force: true }) - let transaction = null; - let currentModelName = null; + let transaction = null + let currentModelName = null try { // Start a transaction - transaction = await models.sequelize.transaction(); - const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()); + transaction = await models.sequelize.transaction() + const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()) for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index]; - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); + const modelOpts = dataModels[index] + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) - currentModelName = modelName; - const model = models[modelName]; - const modelRecords = jsonData[modelName]; + currentModelName = modelName + const model = models[modelName] + const modelRecords = jsonData[modelName] if (modelRecords && modelRecords.length > 0) { logger.info({ component: 'importData', - message: `Importing data for model: ${modelName}`, - }); + message: `Importing data for model: ${modelName}` + }) - await model.bulkCreate(modelRecords, { include, transaction }); + await model.bulkCreate(modelRecords, { include, transaction }) logger.info({ component: 'importData', - message: `Records imported for model: ${modelName} = ${modelRecords.length}`, - }); + message: `Records imported for model: ${modelName} = ${modelRecords.length}` + }) } else { logger.info({ component: 'importData', - message: `No records to import for model: ${modelName}`, - }); + message: `No records to import for model: ${modelName}` + }) } } // commit transaction only if all things went ok logger.info({ component: 'importData', - message: 'committing transaction to database...', - }); - await transaction.commit(); + message: 'committing transaction to database...' + }) + await transaction.commit() } catch (error) { logger.error({ component: 'importData', - message: `Error while writing data of model: ${currentModelName}`, - }); + message: `Error while writing data of model: ${currentModelName}` + }) // rollback all insert operations if (transaction) { logger.info({ component: 'importData', - message: 'rollback database transaction...', - }); - transaction.rollback(); + message: 'rollback database transaction...' + }) + transaction.rollback() } if (error.name && error.errors && error.fields) { // For sequelize validation errors, we throw only fields with data that helps in debugging error, @@ -519,11 +537,11 @@ async function importData(pathToFile, dataModels, logger) { modelName: currentModelName, name: error.name, errors: error.errors, - fields: error.fields, + fields: error.fields }) - ); + ) } else { - throw error; + throw error } } @@ -533,10 +551,10 @@ async function importData(pathToFile, dataModels, logger) { include: [ { model: models.Interview, - as: 'interviews', - }, - ], - }; + as: 'interviews' + } + ] + } const resourceBookingModelOpts = { modelName: 'ResourceBooking', include: [ @@ -546,23 +564,23 @@ async function importData(pathToFile, dataModels, logger) { include: [ { model: models.WorkPeriodPayment, - as: 'payments', - }, - ], - }, - ], - }; - await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger); + as: 'payments' + } + ] + } + ] + } + await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) await indexBulkDataToES( jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger - ); + ) await indexBulkDataToES( resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger - ); + ) } /** @@ -571,74 +589,74 @@ async function importData(pathToFile, dataModels, logger) { * @param {Array} dataModels the data models to export * @param {Object} logger the logger object */ -async function exportData(pathToFile, dataModels, logger) { +async function exportData (pathToFile, dataModels, logger) { logger.info({ component: 'exportData', - message: `Start Saving data to file with path ${pathToFile}....`, - }); + message: `Start Saving data to file with path ${pathToFile}....` + }) - const allModelsRecords = {}; + const allModelsRecords = {} for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index]; - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); - const modelRecords = await models[modelName].findAll({ include }); - const rawRecords = _.map(modelRecords, (r) => r.toJSON()); - allModelsRecords[modelName] = rawRecords; + const modelOpts = dataModels[index] + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) + const modelRecords = await models[modelName].findAll({ include }) + const rawRecords = _.map(modelRecords, (r) => r.toJSON()) + allModelsRecords[modelName] = rawRecords logger.info({ component: 'exportData', - message: `Records loaded for model: ${modelName} = ${rawRecords.length}`, - }); + message: `Records loaded for model: ${modelName} = ${rawRecords.length}` + }) } - fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)); + fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)) logger.info({ component: 'exportData', - message: 'End Saving data to file....', - }); + message: 'End Saving data to file....' + }) } /** * Format a time in milliseconds into a human readable format * @param {Date} milliseconds the number of milliseconds */ -function formatTime(millisec) { - const ms = Math.floor(millisec % 1000); - const secs = Math.floor((millisec / 1000) % 60); - const mins = Math.floor((millisec / (1000 * 60)) % 60); - const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24); - const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7); - const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4); - const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12); - const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)); - - let formattedTime = '0 milliseconds'; +function formatTime (millisec) { + const ms = Math.floor(millisec % 1000) + const secs = Math.floor((millisec / 1000) % 60) + const mins = Math.floor((millisec / (1000 * 60)) % 60) + const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24) + const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7) + const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4) + const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12) + const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)) + + let formattedTime = '0 milliseconds' if (ms > 0) { - formattedTime = `${ms} milliseconds`; + formattedTime = `${ms} milliseconds` } if (secs > 0) { - formattedTime = `${secs} seconds ${formattedTime}`; + formattedTime = `${secs} seconds ${formattedTime}` } if (mins > 0) { - formattedTime = `${mins} minutes ${formattedTime}`; + formattedTime = `${mins} minutes ${formattedTime}` } if (hrs > 0) { - formattedTime = `${hrs} hours ${formattedTime}`; + formattedTime = `${hrs} hours ${formattedTime}` } if (days > 0) { - formattedTime = `${days} days ${formattedTime}`; + formattedTime = `${days} days ${formattedTime}` } if (weeks > 0) { - formattedTime = `${weeks} weeks ${formattedTime}`; + formattedTime = `${weeks} weeks ${formattedTime}` } if (mnths > 0) { - formattedTime = `${mnths} months ${formattedTime}`; + formattedTime = `${mnths} months ${formattedTime}` } if (yrs > 0) { - formattedTime = `${yrs} years ${formattedTime}`; + formattedTime = `${yrs} years ${formattedTime}` } - return formattedTime.trim(); + return formattedTime.trim() } /** @@ -647,30 +665,30 @@ function formatTime(millisec) { * @param {Array} source the array in which to search for the term * @param {Array | String} term the term to search */ -function checkIfExists(source, term) { - let terms; +function checkIfExists (source, term) { + let terms if (!_.isArray(source)) { - throw new Error('Source argument should be an array'); + throw new Error('Source argument should be an array') } - source = source.map((s) => s.toLowerCase()); + source = source.map((s) => s.toLowerCase()) if (_.isString(term)) { - terms = term.toLowerCase().split(' '); + terms = term.toLowerCase().split(' ') } else if (_.isArray(term)) { - terms = term.map((t) => t.toLowerCase()); + terms = term.map((t) => t.toLowerCase()) } else { - throw new Error('Term argument should be either a string or an array'); + throw new Error('Term argument should be either a string or an array') } for (let i = 0; i < terms.length; i++) { if (source.includes(terms[i])) { - return true; + return true } } - return false; + return false } /** @@ -678,10 +696,10 @@ function checkIfExists(source, term) { * @param {Function} fn the async function * @returns {Function} the wrapped function */ -function wrapExpress(fn) { +function wrapExpress (fn) { return function (req, res, next) { - fn(req, res, next).catch(next); - }; + fn(req, res, next).catch(next) + } } /** @@ -689,20 +707,20 @@ function wrapExpress(fn) { * @param obj the object (controller exports) * @returns {Object|Array} the wrapped object */ -function autoWrapExpress(obj) { +function autoWrapExpress (obj) { if (_.isArray(obj)) { - return obj.map(autoWrapExpress); + return obj.map(autoWrapExpress) } if (_.isFunction(obj)) { if (obj.constructor.name === 'AsyncFunction') { - return wrapExpress(obj); + return wrapExpress(obj) } - return obj; + return obj } _.each(obj, (value, key) => { - obj[key] = autoWrapExpress(value); - }); - return obj; + obj[key] = autoWrapExpress(value) + }) + return obj } /** @@ -711,11 +729,11 @@ function autoWrapExpress(obj) { * @param {Number} page the page number * @returns {String} link for the page */ -function getPageLink(req, page) { - const q = _.assignIn({}, req.query, { page }); +function getPageLink (req, page) { + const q = _.assignIn({}, req.query, { page }) return `${req.protocol}://${req.get('Host')}${req.baseUrl}${ req.path - }?${querystring.stringify(q)}`; + }?${querystring.stringify(q)}` } /** @@ -724,31 +742,31 @@ function getPageLink(req, page) { * @param {Object} res the HTTP response * @param {Object} result the operation result */ -function setResHeaders(req, res, result) { - const totalPages = Math.ceil(result.total / result.perPage); +function setResHeaders (req, res, result) { + const totalPages = Math.ceil(result.total / result.perPage) if (result.page > 1) { - res.set('X-Prev-Page', result.page - 1); + res.set('X-Prev-Page', result.page - 1) } if (result.page < totalPages) { - res.set('X-Next-Page', result.page + 1); + res.set('X-Next-Page', result.page + 1) } - res.set('X-Page', result.page); - res.set('X-Per-Page', result.perPage); - res.set('X-Total', result.total); - res.set('X-Total-Pages', totalPages); + res.set('X-Page', result.page) + res.set('X-Per-Page', result.perPage) + res.set('X-Total', result.total) + res.set('X-Total-Pages', totalPages) // set Link header if (totalPages > 0) { let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink( req, totalPages - )}>; rel="last"`; + )}>; rel="last"` if (result.page > 1) { - link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"`; + link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"` } if (result.page < totalPages) { - link += `, <${getPageLink(req, result.page + 1)}>; rel="next"`; + link += `, <${getPageLink(req, result.page + 1)}>; rel="next"` } - res.set('Link', link); + res.set('Link', link) } } @@ -756,30 +774,30 @@ function setResHeaders(req, res, result) { * Get ES Client * @return {Object} Elastic Host Client Instance */ -function getESClient() { +function getESClient () { if (esClients.client) { - return esClients.client; + return esClients.client } - const host = config.esConfig.HOST; - const cloudId = config.esConfig.ELASTICCLOUD.id; + const host = config.esConfig.HOST + const cloudId = config.esConfig.ELASTICCLOUD.id if (cloudId) { // Elastic Cloud configuration esClients.client = new elasticsearch.Client({ cloud: { - id: cloudId, + id: cloudId }, auth: { username: config.esConfig.ELASTICCLOUD.username, - password: config.esConfig.ELASTICCLOUD.password, - }, - }); + password: config.esConfig.ELASTICCLOUD.password + } + }) } else { esClients.client = new elasticsearch.Client({ - node: host, - }); + node: host + }) } - return esClients.client; + return esClients.client } /* @@ -790,8 +808,8 @@ const getM2MToken = async () => { return await m2m.getMachineToken( config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET - ); -}; + ) +} /* * Function to get M2M token for U-Bahn @@ -801,8 +819,8 @@ const getM2MUbahnToken = async () => { return await m2mForUbahn.getMachineToken( config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET - ); -}; + ) +} /** * Function to encode query string @@ -810,17 +828,17 @@ const getM2MUbahnToken = async () => { * @param {String} nesting the nesting string * @returns {String} query string */ -function encodeQueryString(queryObj, nesting = '') { +function encodeQueryString (queryObj, nesting = '') { const pairs = Object.entries(queryObj).map(([key, val]) => { // Handle the nested, recursive case, where the value to encode is an object itself if (typeof val === 'object') { - return encodeQueryString(val, nesting + `${key}.`); + return encodeQueryString(val, nesting + `${key}.`) } else { // Handle base case, where the value to encode is simply a string. - return [nesting + key, val].map(querystring.escape).join('='); + return [nesting + key, val].map(querystring.escape).join('=') } - }); - return pairs.join('&'); + }) + return pairs.join('&') } /** @@ -828,31 +846,31 @@ function encodeQueryString(queryObj, nesting = '') { * @param {Integer} externalId the legacy user id * @returns {Array} the users found */ -async function listUsersByExternalId(externalId) { +async function listUsersByExternalId (externalId) { // return empty list if externalId is null or undefined if (!!externalId !== true) { - return []; + return [] } - const token = await getM2MUbahnToken(); + const token = await getM2MUbahnToken() const q = { enrich: true, externalProfile: { organizationId: config.ORG_ID, - externalId, - }, - }; - const url = `${config.TC_API}/users?${encodeQueryString(q)}`; + externalId + } + } + const url = `${config.TC_API}/users?${encodeQueryString(q)}` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listUserByExternalId', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -860,14 +878,14 @@ async function listUsersByExternalId(externalId) { * @param {Integer} externalId the legacy user id * @returns {Object} the user */ -async function getUserByExternalId(externalId) { - const users = await listUsersByExternalId(externalId); +async function getUserByExternalId (externalId) { + const users = await listUsersByExternalId(externalId) if (_.isEmpty(users)) { throw new errors.NotFoundError( `externalId: ${externalId} "user" not found` - ); + ) } - return users[0]; + return users[0] } /** @@ -876,24 +894,24 @@ async function getUserByExternalId(externalId) { * @params {Object} payload the payload * @params {Object} options the extra options to control the function */ -async function postEvent(topic, payload, options = {}) { +async function postEvent (topic, payload, options = {}) { logger.debug({ component: 'helper', context: 'postEvent', message: `Posting event to Kafka topic ${topic}, ${JSON.stringify( payload - )}`, - }); - const client = getBusApiClient(); + )}` + }) + const client = getBusApiClient() const message = { topic, originator: config.KAFKA_MESSAGE_ORIGINATOR, timestamp: new Date().toISOString(), 'mime-type': 'application/json', - payload, - }; - await client.postEvent(message); - await eventDispatcher.handleEvent(topic, { value: payload, options }); + payload + } + await client.postEvent(message) + await eventDispatcher.handleEvent(topic, { value: payload, options }) } /** @@ -902,11 +920,11 @@ async function postEvent(topic, payload, options = {}) { * @param {Object} err the err * @returns {Boolean} the result */ -function isDocumentMissingException(err) { +function isDocumentMissingException (err) { if (err.statusCode === 404 && err instanceof ESResponseError) { - return true; + return true } - return false; + return false } /** @@ -915,34 +933,34 @@ function isDocumentMissingException(err) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getProjects(currentUser, criteria = {}) { - let token; +async function getProjects (currentUser, criteria = {}) { + let token if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken(); - token = `Bearer ${m2mToken}`; + const m2mToken = await getM2MToken() + token = `Bearer ${m2mToken}` } else { - token = currentUser.jwtToken; + token = currentUser.jwtToken } - const url = `${config.TC_API}/projects?type=talent-as-a-service`; + const url = `${config.TC_API}/projects?type=talent-as-a-service` const res = await request .get(url) .query(criteria) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getProjects', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) const result = _.map(res.body, (item) => { - return _.pick(item, ['id', 'name', 'invites', 'members']); - }); + return _.pick(item, ['id', 'name', 'invites', 'members']) + }) return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result, - }; + result + } } /** @@ -951,24 +969,24 @@ async function getProjects(currentUser, criteria = {}) { * @param {String} userId the legacy user id * @returns {Object} the user */ -async function getTopcoderUserById(userId) { - const token = await getM2MToken(); +async function getTopcoderUserById (userId) { + const token = await getM2MToken() const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `id=${userId}` }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getTopcoderUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - const user = _.get(res.body, 'result.content[0]'); + message: `response body: ${JSON.stringify(res.body)}` + }) + const user = _.get(res.body, 'result.content[0]') if (!user) { throw new errors.NotFoundError( `userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}` - ); + ) } - return user; + return user } /** @@ -976,31 +994,31 @@ async function getTopcoderUserById(userId) { * @param {String} userId the user id * @returns the request result */ -async function getUserById(userId, enrich) { - const token = await getM2MUbahnToken(); +async function getUserById (userId, enrich) { + const token = await getM2MUbahnToken() const res = await request .get(`${config.TC_API}/users/${userId}` + (enrich ? '?enrich=true' : '')) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) - const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']); + const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']) if (enrich) { user.skills = (res.body.skills || []).map((skillObj) => _.pick(skillObj.skill, ['id', 'name']) - ); - const attributes = _.get(res, 'body.attributes', []); + ) + const attributes = _.get(res, 'body.attributes', []) user.attributes = _.map(attributes, (attr) => _.pick(attr, ['id', 'value', 'attribute.id', 'attribute.name']) - ); + ) } - return user; + return user } /** @@ -1008,19 +1026,19 @@ async function getUserById(userId, enrich) { * @param {Object} data the user data * @returns the request result */ -async function createUbahnUser({ handle, firstName, lastName }) { - const token = await getM2MUbahnToken(); +async function createUbahnUser ({ handle, firstName, lastName }) { + const token = await getM2MUbahnToken() const res = await request .post(`${config.TC_API}/users`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ handle, firstName, lastName }); + .send({ handle, firstName, lastName }) localLogger.debug({ context: 'createUbahnUser', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id']) } /** @@ -1028,21 +1046,21 @@ async function createUbahnUser({ handle, firstName, lastName }) { * @param {String} userId the user id(with uuid format) * @param {Object} data the profile data */ -async function createUserExternalProfile( +async function createUserExternalProfile ( userId, { organizationId, externalId } ) { - const token = await getM2MUbahnToken(); + const token = await getM2MUbahnToken() const res = await request .post(`${config.TC_API}/users/${userId}/externalProfiles`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ organizationId, externalId: String(externalId) }); + .send({ organizationId, externalId: String(externalId) }) localLogger.debug({ context: 'createUserExternalProfile', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) } /** @@ -1050,23 +1068,23 @@ async function createUserExternalProfile( * @param {Array} handles the handle array * @returns the request result */ -async function getMembers(handles) { - const token = await getM2MToken(); +async function getMembers (handles) { + const token = await getM2MToken() const handlesStr = _.map(handles, (handle) => { - return '%22' + handle.toLowerCase() + '%22'; - }).join(','); - const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]`; + return '%22' + handle.toLowerCase() + '%22' + }).join(',') + const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getMembers', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -1075,36 +1093,36 @@ async function getMembers(handles) { * @param {Number} id project id * @returns the request result */ -async function getProjectById(currentUser, id) { - let token; +async function getProjectById (currentUser, id) { + let token if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken(); - token = `Bearer ${m2mToken}`; + const m2mToken = await getM2MToken() + token = `Bearer ${m2mToken}` } else { - token = currentUser.jwtToken; + token = currentUser.jwtToken } - const url = `${config.TC_API}/projects/${id}`; + const url = `${config.TC_API}/projects/${id}` try { const res = await request .get(url) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getProjectById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id', 'name', 'invites', 'members']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id', 'name', 'invites', 'members']) } catch (err) { if (err.status === HttpStatus.FORBIDDEN) { throw new errors.ForbiddenError( `You are not allowed to access the project with id ${id}` - ); + ) } if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${id} project not found`); + throw new errors.NotFoundError(`id: ${id} project not found`) } - throw err; + throw err } } @@ -1115,33 +1133,33 @@ async function getProjectById(currentUser, id) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getTopcoderSkills(criteria) { - const token = await getM2MUbahnToken(); +async function getTopcoderSkills (criteria) { + const token = await getM2MUbahnToken() try { const res = await request .get(`${config.TC_API}/skills`) .query({ skillProviderId: config.TOPCODER_SKILL_PROVIDER_ID, - ...criteria, + ...criteria }) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getTopcoderSkills', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result: res.body, - }; + result: res.body + } } catch (err) { if (err.status === HttpStatus.BAD_REQUEST) { - throw new errors.BadRequestError(err.response.body.message); + throw new errors.BadRequestError(err.response.body.message) } - throw err; + throw err } } @@ -1150,18 +1168,18 @@ async function getTopcoderSkills(criteria) { * @param {String} skillId the skill Id * @returns the request result */ -async function getSkillById(skillId) { - const token = await getM2MUbahnToken(); +async function getSkillById (skillId) { + const token = await getM2MUbahnToken() const res = await request .get(`${config.TC_API}/skills/${skillId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getSkillById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id', 'name']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id', 'name']) } /** @@ -1174,22 +1192,22 @@ async function getSkillById(skillId) { * @params {Object} currentUser the user who perform this operation * @returns {String} the ubahn user id */ -async function ensureUbahnUserId(currentUser) { +async function ensureUbahnUserId (currentUser) { try { - return (await getUserByExternalId(currentUser.userId)).id; + return (await getUserByExternalId(currentUser.userId)).id } catch (err) { if (!(err instanceof errors.NotFoundError)) { - throw err; + throw err } - const topcoderUser = await getTopcoderUserById(currentUser.userId); + const topcoderUser = await getTopcoderUserById(currentUser.userId) const user = await createUbahnUser( _.pick(topcoderUser, ['handle', 'firstName', 'lastName']) - ); + ) await createUserExternalProfile(user.id, { organizationId: config.ORG_ID, - externalId: currentUser.userId, - }); - return user.id; + externalId: currentUser.userId + }) + return user.id } } @@ -1199,8 +1217,8 @@ async function ensureUbahnUserId(currentUser) { * @param {String} jobId the job id * @returns {Object} the job data */ -async function ensureJobById(jobId) { - return models.Job.findById(jobId); +async function ensureJobById (jobId) { + return models.Job.findById(jobId) } /** @@ -1209,8 +1227,8 @@ async function ensureJobById(jobId) { * @param {String} resourceBookingId the resourceBooking id * @returns {Object} the resourceBooking data */ -async function ensureResourceBookingById(resourceBookingId) { - return models.ResourceBooking.findById(resourceBookingId); +async function ensureResourceBookingById (resourceBookingId) { + return models.ResourceBooking.findById(resourceBookingId) } /** @@ -1218,8 +1236,8 @@ async function ensureResourceBookingById(resourceBookingId) { * @param {String} workPeriodId the workPeriod id * @returns the workPeriod data */ -async function ensureWorkPeriodById(workPeriodId) { - return models.WorkPeriod.findById(workPeriodId); +async function ensureWorkPeriodById (workPeriodId) { + return models.WorkPeriod.findById(workPeriodId) } /** @@ -1228,24 +1246,24 @@ async function ensureWorkPeriodById(workPeriodId) { * @param {String} jobId the user id * @returns {Object} the user data */ -async function ensureUserById(userId) { - const token = await getM2MUbahnToken(); +async function ensureUserById (userId) { + const token = await getM2MUbahnToken() try { const res = await request .get(`${config.TC_API}/users/${userId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'ensureUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${userId} "user" not found`); + throw new errors.NotFoundError(`id: ${userId} "user" not found`) } - throw err; + throw err } } @@ -1254,12 +1272,12 @@ async function ensureUserById(userId) { * * @returns {Object} the M2M auth user */ -function getAuditM2Muser() { +function getAuditM2Muser () { return { isMachine: true, userId: config.m2m.M2M_AUDIT_USER_ID, - handle: config.m2m.M2M_AUDIT_HANDLE, - }; + handle: config.m2m.M2M_AUDIT_HANDLE + } } /** @@ -1271,24 +1289,24 @@ function getAuditM2Muser() { * @param {Number} projectId project id * @returns the result */ -async function checkIsMemberOfProject(userId, projectId) { - const m2mToken = await getM2MToken(); +async function checkIsMemberOfProject (userId, projectId) { + const m2mToken = await getM2MToken() const res = await request .get(`${config.TC_API}/projects/${projectId}`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const memberIdList = _.map(res.body.members, 'userId'); + .set('Accept', 'application/json') + const memberIdList = _.map(res.body.members, 'userId') localLogger.debug({ context: 'checkIsMemberOfProject', message: `the members of project ${projectId}: ${JSON.stringify( memberIdList - )}, authUserId: ${JSON.stringify(userId)}`, - }); + )}, authUserId: ${JSON.stringify(userId)}` + }) if (!memberIdList.includes(userId)) { throw new errors.UnauthorizedError( `userId: ${userId} the user is not a member of project ${projectId}` - ); + ) } } @@ -1298,24 +1316,24 @@ async function checkIsMemberOfProject(userId, projectId) { * @param {Array} handles the array of handles * @returns {Array} the member details */ -async function getMemberDetailsByHandles(handles) { +async function getMemberDetailsByHandles (handles) { if (!handles.length) { - return []; + return [] } - const token = await getM2MToken(); + const token = await getM2MToken() const res = await request .get(`${config.TOPCODER_MEMBERS_API}/`) .query({ 'handlesLower[]': handles.map(handle => handle.toLowerCase()), - fields: 'userId,handle,handleLower,firstName,lastName,email', + fields: 'userId,handle,handleLower,firstName,lastName,email' }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getMemberDetailsByHandles', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -1324,7 +1342,7 @@ async function getMemberDetailsByHandles(handles) { * @param {String} handle the user handle * @returns {Object} the member details */ -async function getMemberDetailsByHandle(handle) { +async function getMemberDetailsByHandle (handle) { const [memberDetails] = await getMemberDetailsByHandles([handle]) if (!memberDetails) { @@ -1341,20 +1359,20 @@ async function getMemberDetailsByHandle(handle) { * @param {String} email the email * @returns {Array} the member details */ -async function _getMemberDetailsByEmail(token, email) { +async function _getMemberDetailsByEmail (token, email) { const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `email=${email}`, - fields: 'handle,id,email,firstName,lastName', + fields: 'handle,id,email,firstName,lastName' }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: '_getMemberDetailsByEmail', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res.body, 'result.content'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res.body, 'result.content') } /** @@ -1364,25 +1382,25 @@ async function _getMemberDetailsByEmail(token, email) { * @param {Array} emails the array of emails * @returns {Array} the member details */ -async function getMemberDetailsByEmails(emails) { - const token = await getM2MToken(); +async function getMemberDetailsByEmails (emails) { + const token = await getM2MToken() const limiter = new Bottleneck({ - maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API, - }); + maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API + }) const membersArray = await Promise.all( emails.map((email) => limiter.schedule(() => _getMemberDetailsByEmail(token, email).catch((error) => { localLogger.error({ context: 'getMemberDetailsByEmails', - message: error.message, - }); - return []; + message: error.message + }) + return [] }) ) ) - ); - return _.flatten(membersArray); + ) + return _.flatten(membersArray) } /** @@ -1393,20 +1411,20 @@ async function getMemberDetailsByEmails(emails) { * @param {Object} criteria the filtering criteria * @returns {Object} the member created */ -async function createProjectMember(projectId, data, criteria) { - const m2mToken = await getM2MToken(); +async function createProjectMember (projectId, data, criteria) { + const m2mToken = await getM2MToken() const { body: member } = await request .post(`${config.TC_API}/projects/${projectId}/members`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') .query(criteria) - .send(data); + .send(data) localLogger.debug({ context: 'createProjectMember', - message: `response body: ${JSON.stringify(member)}`, - }); - return member; + message: `response body: ${JSON.stringify(member)}` + }) + return member } /** @@ -1416,21 +1434,21 @@ async function createProjectMember(projectId, data, criteria) { * @param {Object} criteria the search criteria * @returns {Array} the project members */ -async function listProjectMembers(currentUser, projectId, criteria = {}) { +async function listProjectMembers (currentUser, projectId, criteria = {}) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken const { body: members } = await request .get(`${config.TC_API}/projects/${projectId}/members`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listProjectMembers', - message: `response body: ${JSON.stringify(members)}`, - }); - return members; + message: `response body: ${JSON.stringify(members)}` + }) + return members } /** @@ -1440,21 +1458,21 @@ async function listProjectMembers(currentUser, projectId, criteria = {}) { * @param {Object} criteria the search criteria * @returns {Array} the member invites */ -async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { +async function listProjectMemberInvites (currentUser, projectId, criteria = {}) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken const { body: invites } = await request .get(`${config.TC_API}/projects/${projectId}/invites`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listProjectMemberInvites', - message: `response body: ${JSON.stringify(invites)}`, - }); - return invites; + message: `response body: ${JSON.stringify(invites)}` + }) + return invites } /** @@ -1464,24 +1482,24 @@ async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { * @param {String} projectMemberId the id of the project member * @returns {undefined} */ -async function deleteProjectMember(currentUser, projectId, projectMemberId) { +async function deleteProjectMember (currentUser, projectId, projectMemberId) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken try { await request .delete( `${config.TC_API}/projects/${projectId}/members/${projectMemberId}` ) - .set('Authorization', token); + .set('Authorization', token) } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { throw new errors.NotFoundError( `projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}` - ); + ) } - throw err; + throw err } } @@ -1491,13 +1509,13 @@ async function deleteProjectMember(currentUser, projectId, projectMemberId) { * @param {String} attributeName Requested attribute name, e.g. "email" * @returns attribute value */ -function getUserAttributeValue(user, attributeName) { - const attributes = _.get(user, 'attributes', []); +function getUserAttributeValue (user, attributeName) { + const attributes = _.get(user, 'attributes', []) const targetAttribute = _.find( attributes, (a) => a.attribute.name === attributeName - ); - return _.get(targetAttribute, 'value'); + ) + return _.get(targetAttribute, 'value') } /** @@ -1507,34 +1525,34 @@ function getUserAttributeValue(user, attributeName) { * @param {String} token m2m token * @returns {Object} the challenge created */ -async function createChallenge(data, token) { +async function createChallenge (data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/challenges`; + const url = `${config.TC_API}/challenges` localLogger.debug({ context: 'createChallenge', - message: `EndPoint: POST ${url}`, - }); + message: `EndPoint: POST ${url}` + }) localLogger.debug({ context: 'createChallenge', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: challenge, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createChallenge', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'createChallenge', - message: `Response Body: ${JSON.stringify(challenge)}`, - }); - return challenge; + message: `Response Body: ${JSON.stringify(challenge)}` + }) + return challenge } /** @@ -1545,34 +1563,34 @@ async function createChallenge(data, token) { * @param {String} token m2m token * @returns {Object} the challenge updated */ -async function updateChallenge(challengeId, data, token) { +async function updateChallenge (challengeId, data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/challenges/${challengeId}`; + const url = `${config.TC_API}/challenges/${challengeId}` localLogger.debug({ context: 'updateChallenge', - message: `EndPoint: PATCH ${url}`, - }); + message: `EndPoint: PATCH ${url}` + }) localLogger.debug({ context: 'updateChallenge', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: challenge, status: httpStatus } = await request .patch(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'updateChallenge', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'updateChallenge', - message: `Response Body: ${JSON.stringify(challenge)}`, - }); - return challenge; + message: `Response Body: ${JSON.stringify(challenge)}` + }) + return challenge } /** @@ -1582,34 +1600,34 @@ async function updateChallenge(challengeId, data, token) { * @param {String} token m2m token * @returns {Object} the resource created */ -async function createChallengeResource(data, token) { +async function createChallengeResource (data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/resources`; + const url = `${config.TC_API}/resources` localLogger.debug({ context: 'createChallengeResource', - message: `EndPoint: POST ${url}`, - }); + message: `EndPoint: POST ${url}` + }) localLogger.debug({ context: 'createChallengeResource', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: resource, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createChallengeResource', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'createChallengeResource', - message: `Response Body: ${JSON.stringify(resource)}`, - }); - return resource; + message: `Response Body: ${JSON.stringify(resource)}` + }) + return resource } /** @@ -1618,40 +1636,40 @@ async function createChallengeResource(data, token) { * @param {Date} end end date of the resource booking * @returns {Array<{startDate:Date, endDate:Date, daysWorked:number}>} information about workPeriods */ -function extractWorkPeriods(start, end) { +function extractWorkPeriods (start, end) { // calculate maximum possible daysWorked for a week - function getDaysWorked(week) { + function getDaysWorked (week) { if (weeks === 1) { - return Math.min(endDay, 5) - Math.max(startDay, 1) + 1; + return Math.min(endDay, 5) - Math.max(startDay, 1) + 1 } else if (week === 0) { - return Math.min(6 - startDay, 5); + return Math.min(6 - startDay, 5) } else if (week === weeks - 1) { - return Math.min(endDay, 5); - } else return 5; + return Math.min(endDay, 5) + } else return 5 } - const periods = []; + const periods = [] if (_.isNil(start) || _.isNil(end)) { - return periods; + return periods } - const startDate = moment(start); - const startDay = startDate.get('day'); - startDate.set('day', 0).startOf('day'); + const startDate = moment(start) + const startDay = startDate.get('day') + startDate.set('day', 0).startOf('day') - const endDate = moment(end); - const endDay = endDate.get('day'); - endDate.set('day', 6).endOf('day'); + const endDate = moment(end) + const endDay = endDate.get('day') + endDate.set('day', 6).endOf('day') - const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7; + const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7 for (let i = 0; i < weeks; i++) { periods.push({ startDate: startDate.format('YYYY-MM-DD'), endDate: startDate.add(6, 'day').format('YYYY-MM-DD'), - daysWorked: getDaysWorked(i), - }); - startDate.add(1, 'day'); + daysWorked: getDaysWorked(i) + }) + startDate.add(1, 'day') } - return periods; + return periods } /** @@ -1660,19 +1678,19 @@ function extractWorkPeriods(start, end) { * @param {String} userHandle user handle * @returns {String} email address of the user */ -async function getUserByHandle(userHandle) { - const token = await getM2MToken(); - const url = `${config.TC_API}/members/${userHandle}`; +async function getUserByHandle (userHandle) { + const token = await getM2MToken() + const url = `${config.TC_API}/members/${userHandle}` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getUserByHandle', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res, 'body'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res, 'body') } /** @@ -1681,14 +1699,14 @@ async function getUserByHandle(userHandle) { * @param {*} object of json that would be replaced in string * @returns */ -async function substituteStringByObject(string, object) { +async function substituteStringByObject (string, object) { for (var key in object) { if (!Object.prototype.hasOwnProperty.call(object, key)) { - continue; + continue } - string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]); + string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]) } - return string; + return string } /** @@ -1696,19 +1714,19 @@ async function substituteStringByObject(string, object) { * @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 (currentUser, data) { + const token = currentUser.jwtToken const res = await request .post(`${config.TC_API}/projects/`) .set('Authorization', token) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createProject', - message: `response body: ${JSON.stringify(res)}`, - }); - return _.get(res, 'body'); + message: `response body: ${JSON.stringify(res)}` + }) + return _.get(res, 'body') } module.exports = { @@ -1727,9 +1745,9 @@ module.exports = { getUserId: async (userId) => { // check m2m user id if (userId === config.m2m.M2M_AUDIT_USER_ID) { - return config.m2m.M2M_AUDIT_USER_ID; + return config.m2m.M2M_AUDIT_USER_ID } - return ensureUbahnUserId({ userId }); + return ensureUbahnUserId({ userId }) }, getUserByExternalId, getM2MToken, @@ -1763,5 +1781,5 @@ module.exports = { extractWorkPeriods, getUserByHandle, substituteStringByObject, - createProject, -}; + createProject +} diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index f5c40206..8871de9c 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -472,7 +472,6 @@ async function searchResourceBookings (currentUser, criteria, options = { return criteria.sortOrder = 'desc' } try { - throw new Error('fallback to DB') const esQuery = { index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), _source_includes: queryOpt.include, @@ -484,12 +483,10 @@ async function searchResourceBookings (currentUser, criteria, options = { return } }, from: (page - 1) * perPage, - size: perPage + size: perPage, + sort: [] } } - if (!queryOpt.sortByWP) { - esQuery.body.sort = [{ [criteria.sortBy === 'id' ? '_id' : criteria.sortBy]: { order: criteria.sortOrder } }] - } // change the date format to match with index schema if (criteria.startDate) { criteria.startDate = moment(criteria.startDate).format('YYYY-MM-DD') @@ -503,6 +500,17 @@ async function searchResourceBookings (currentUser, criteria, options = { return if (criteria['workPeriods.endDate']) { criteria['workPeriods.endDate'] = moment(criteria['workPeriods.endDate']).format('YYYY-MM-DD') } + const sort = { [criteria.sortBy === 'id' ? '_id' : criteria.sortBy]: { order: criteria.sortOrder } } + if (queryOpt.sortByWP) { + const nestedSortFilter = {} + if (criteria['workPeriods.startDate']) { + nestedSortFilter.term = { 'workPeriods.startDate': criteria['workPeriods.startDate'] } + } else if (criteria['workPeriods.endDate']) { + nestedSortFilter.term = { 'workPeriods.endDate': criteria['workPeriods.endDate'] } + } + sort[criteria.sortBy].nested = { path: 'workPeriods', filter: nestedSortFilter } + } + esQuery.body.sort.push(sort) // Apply ResourceBooking filters _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId']), (value, key) => { esQuery.body.query.bool.must.push({ @@ -522,10 +530,10 @@ async function searchResourceBookings (currentUser, criteria, options = { return }] } // Apply WorkPeriod filters - const workPeriodFilters = ['workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle'] - if (_.intersection(criteria, workPeriodFilters).length > 0) { + const workPeriodFilters = _.pick(criteria, ['workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle']) + if (!_.isEmpty(workPeriodFilters)) { const workPeriodsMust = [] - _.each(_.pick(criteria, workPeriodFilters), (value, key) => { + _.each(workPeriodFilters, (value, key) => { workPeriodsMust.push({ term: { [key]: { @@ -545,23 +553,16 @@ async function searchResourceBookings (currentUser, criteria, options = { return logger.debug({ component: 'ResourceBookingService', context: 'searchResourceBookings', message: `Query: ${JSON.stringify(esQuery)}` }) const { body } = await esClient.search(esQuery) - let resourceBookings = _.map(body.hits.hits, '_source') + const resourceBookings = _.map(body.hits.hits, '_source') // ESClient will return ResourceBookings with it's all nested WorkPeriods // We re-apply WorkPeriod filters - _.each(_.pick(criteria, workPeriodFilters), (value, key) => { + _.each(workPeriodFilters, (value, key) => { key = key.split('.')[1] _.each(resourceBookings, r => { r.workPeriods = _.filter(r.workPeriods, { [key]: value }) }) }) - // If sorting criteria is WorkPeriod field, we have to sort manually - if (queryOpt.sortByWP) { - const sorts = criteria.sortBy.split('.') - resourceBookings = _.sortBy(resourceBookings, [`${sorts[0]}[0].${sorts[1]}`]) - if (criteria.sortOrder === 'desc') { - resourceBookings = _.reverse(resourceBookings) - } - } + return { total: body.hits.total.value, page, @@ -614,18 +615,26 @@ async function searchResourceBookings (currentUser, criteria, options = { return } // Apply sorting criteria if (!queryOpt.sortByWP) { - queryCriteria.order = [[criteria.sortBy, criteria.sortOrder]] + queryCriteria.order = [[criteria.sortBy, `${criteria.sortOrder} NULLS LAST`]] } else { - queryCriteria.order = [[{ model: WorkPeriod, as: 'workPeriods' }, _.split(criteria.sortBy, '.')[1], criteria.sortOrder]] - } - const resourceBookings = await ResourceBooking.findAll(queryCriteria) - const total = await ResourceBooking.count(_.omit(queryCriteria, ['limit', 'offset', 'attributes', 'order'])) + queryCriteria.subQuery = false + queryCriteria.order = [[{ model: WorkPeriod, as: 'workPeriods' }, _.split(criteria.sortBy, '.')[1], `${criteria.sortOrder} NULLS LAST`]] + } + const result = await ResourceBooking.findAll(queryCriteria) + let countQuery + countQuery = _.omit(queryCriteria, ['limit', 'offset', 'attributes', 'order']) + if (queryOpt.withWorkPeriods && !queryCriteria.include[0].required) { + countQuery = _.omit(countQuery, ['include']) + } + countQuery.subQuery = false + countQuery.group = ['ResourceBooking.id'] + const total = await ResourceBooking.count(countQuery) return { fromDb: true, - total, + total: total.length, page, perPage, - result: resourceBookings + result } } From 83b994c2dfdc857971c3af9491d268b0144f9443 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Mon, 31 May 2021 18:18:37 +0300 Subject: [PATCH 22/86] role endpoint added --- README.md | 6 +- app-constants.js | 8 +- config/default.js | 9 + data/demo-data.json | 260 +- ...coder-bookings-api.postman_collection.json | 3771 ++++++- docs/swagger.yaml | 476 + ...topcoder-bookings.postman_environment.json | 56 +- local/kafka-client/topics.txt | 3 + migrations/2021-05-27-1-role-table-create.js | 146 + .../2021-05-27-2-job-add-roleIds-field.js | 19 + package.json | 1 + scripts/data/exportData.js | 2 +- scripts/data/importData.js | 2 +- scripts/es/createIndex.js | 3 +- scripts/es/deleteIndex.js | 3 +- scripts/es/reIndexAll.js | 1 + scripts/es/reIndexRoles.js | 37 + src/bootstrap.js | 3 +- src/common/helper.js | 1115 +- src/controllers/RoleController.js | 59 + src/controllers/TeamController.js | 62 +- src/eventHandlers/RoleEventHandler.js | 64 + src/eventHandlers/index.js | 4 +- src/models/Job.js | 6 + src/models/Role.js | 165 + src/routes/RoleRoutes.js | 41 + src/routes/TeamRoutes.js | 48 +- src/services/InterviewService.js | 4 +- src/services/JobService.js | 42 +- src/services/ResourceBookingService.js | 1 + src/services/RoleService.js | 305 + src/services/TeamService.js | 390 +- taas-apis.patch | 9418 +++++++++++++++++ 33 files changed, 15661 insertions(+), 869 deletions(-) create mode 100644 migrations/2021-05-27-1-role-table-create.js create mode 100644 migrations/2021-05-27-2-job-add-roleIds-field.js create mode 100644 scripts/es/reIndexRoles.js create mode 100644 src/controllers/RoleController.js create mode 100644 src/eventHandlers/RoleEventHandler.js create mode 100644 src/models/Role.js create mode 100644 src/routes/RoleRoutes.js create mode 100644 src/services/RoleService.js create mode 100644 taas-apis.patch diff --git a/README.md b/README.md index 5e3895c2..aa36c621 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,9 @@ tc-taas-es-processor | [2021-04-09T21:20:19.035Z] app INFO : Starting kafka consumer tc-taas-es-processor | 2021-04-09T21:20:21.292Z INFO no-kafka-client Joined group taas-es-processor generationId 1 as no-kafka-client-076538fc-60dd-4ca4-a2b9-520bdf73bc9e tc-taas-es-processor | 2021-04-09T21:20:21.293Z INFO no-kafka-client Elected as group leader + tc-taas-es-processor | 2021-04-09T21:20:21.449Z DEBUG no-kafka-client Subscribed to taas.role.update:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-04-09T21:20:21.450Z DEBUG no-kafka-client Subscribed to taas.role.delete:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-04-09T21:20:21.451Z DEBUG no-kafka-client Subscribed to taas.role.requested:0 offset 0 leader kafka:9093 tc-taas-es-processor | 2021-04-09T21:20:21.452Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.create:0 offset 0 leader kafka:9093 tc-taas-es-processor | 2021-04-09T21:20:21.455Z DEBUG no-kafka-client Subscribed to taas.job.create:0 offset 0 leader kafka:9093 tc-taas-es-processor | 2021-04-09T21:20:21.456Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.delete:0 offset 0 leader kafka:9093 @@ -103,7 +106,7 @@ tc-taas-es-processor | 2021-04-09T21:20:21.473Z DEBUG no-kafka-client Subscribed to taas.job.update:0 offset 0 leader kafka:9093 tc-taas-es-processor | 2021-04-09T21:20:21.474Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.update:0 offset 0 leader kafka:9093 tc-taas-es-processor | [2021-04-09T21:20:21.475Z] app INFO : Initialized....... - tc-taas-es-processor | [2021-04-09T21:20:21.479Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete,taas.workperiod.create,taas.workperiod.update,taas.workperiod.delete,taas.workperiodpayment.create,taas.workperiodpayment.update,taas.workperiodpayment.delete + tc-taas-es-processor | [2021-04-09T21:20:21.479Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete,taas.workperiod.create,taas.workperiod.update,taas.workperiod.delete,taas.workperiodpayment.create,taas.workperiodpayment.update,taas.interview.requested,taas.interview.update,taas.interview.bulkUpdate,taas.role.requested,taas.role.update,taas.role.delete tc-taas-es-processor | [2021-04-09T21:20:21.480Z] app INFO : Kick Start....... tc-taas-es-processor | ********** Topcoder Health Check DropIn listening on port 3001 tc-taas-es-processor | Topcoder Health Check DropIn started and ready to roll @@ -194,6 +197,7 @@ To be able to change and test `taas-es-processor` locally you can follow the nex | `npm run index:jobs ` | Indexes job data from db into ES, if jobId is not given all data is indexed. Use `-- --force` flag to skip confirmation | | `npm run index:job-candidates ` | Indexes job candidate data from db into ES, if jobCandidateId is not given all data is indexed. Use `-- --force` flag to skip confirmation | | `npm run index:resource-bookings ` | Indexes resource bookings data from db into ES, if resourceBookingsId is not given all data is indexed. Use `-- --force` flag to skip confirmation | +| `npm run index:roles ` | Indexes roles data from db into ES, if roleId is not given all data is indexed. Use `-- --force` flag to skip confirmation | | `npm run services:up` | Start services via docker-compose for local development. | | `npm run services:down` | Stop services via docker-compose for local development. | | `npm run services:logs -- -f ` | View logs of some service inside docker-compose. | diff --git a/app-constants.js b/app-constants.js index 534e46de..9b577728 100644 --- a/app-constants.js +++ b/app-constants.js @@ -49,7 +49,13 @@ const Scopes = { READ_INTERVIEW: 'read:taas-interviews', CREATE_INTERVIEW: 'create:taas-interviews', UPDATE_INTERVIEW: 'update:taas-interviews', - ALL_INTERVIEW: 'all:taas-interviews' + ALL_INTERVIEW: 'all:taas-interviews', + // role + READ_ROLE: 'read:taas-roles', + CREATE_ROLE: 'create:taas-roles', + UPDATE_ROLE: 'update:taas-roles', + DELETE_ROLE: 'delete:taas-roles', + ALL_ROLE: 'all:taas-roles' } // Interview related constants diff --git a/config/default.js b/config/default.js index 2b5ca7ba..cf2a8a4f 100644 --- a/config/default.js +++ b/config/default.js @@ -76,6 +76,8 @@ module.exports = { ES_INDEX_JOB_CANDIDATE: process.env.ES_INDEX_JOB_CANDIDATE || 'job_candidate', // the resource booking index ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking', + // the role index + ES_INDEX_ROLE: process.env.ES_INDEX_ROLE || 'role', // the max bulk size in MB for ES indexing MAX_BULK_REQUEST_SIZE_MB: process.env.MAX_BULK_REQUEST_SIZE_MB || 20, @@ -131,6 +133,13 @@ module.exports = { TAAS_INTERVIEW_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_UPDATE_TOPIC || 'taas.interview.update', // the interview bulk update Kafka message topic TAAS_INTERVIEW_BULK_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_BULK_UPDATE_TOPIC || 'taas.interview.bulkUpdate', + // topics for role service + // the create role entity Kafka message topic + TAAS_ROLE_CREATE_TOPIC: process.env.TAAS_ROLE_CREATE_TOPIC || 'taas.role.requested', + // the update role entity Kafka message topic + TAAS_ROLE_UPDATE_TOPIC: process.env.TAAS_ROLE_UPDATE_TOPIC || 'taas.role.update', + // the delete role entity Kafka message topic + TAAS_ROLE_DELETE_TOPIC: process.env.TAAS_ROLE_DELETE_TOPIC || 'taas.role.delete', // the Kafka message topic for sending email EMAIL_TOPIC: process.env.EMAIL_TOPIC || 'external.action.email', diff --git a/data/demo-data.json b/data/demo-data.json index e0733443..5f6c4c07 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -20,6 +20,7 @@ ], "status": "in-review", "isApplicationPageActive": false, + "roleIds": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:21:10.394Z", @@ -45,6 +46,7 @@ ], "status": "in-review", "isApplicationPageActive": false, + "roleIds": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:11:26.934Z", @@ -70,6 +72,7 @@ ], "status": "in-review", "isApplicationPageActive": false, + "roleIds": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:23:18.595Z", @@ -95,6 +98,7 @@ ], "status": "in-review", "isApplicationPageActive": false, + "roleIds": null, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-09T21:12:09.293Z", @@ -181,18 +185,29 @@ "interviews": [ { "id": "077aa2ca-5b60-4ad9-a965-1b37e08a5046", + "xaiId": null, "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", - "googleCalendarId": null, - "customMessage": null, - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 1, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Completed", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:16:10.887Z", - "updatedAt": "2021-05-09T21:16:10.887Z" + "updatedAt": "2021-05-09T21:16:10.887Z", + "deletedAt": null } ] }, @@ -210,33 +225,55 @@ "interviews": [ { "id": "b1f7ba76-640f-47e2-9463-59e51b51ec60", + "xaiId": null, "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 2, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Scheduling", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:17:23.517Z", - "updatedAt": "2021-05-09T21:17:23.517Z" + "updatedAt": "2021-05-09T21:17:23.517Z", + "deletedAt": null }, { "id": "3144fa65-ea1a-4bec-81b0-7cb1c8845826", + "xaiId": null, "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": null, - "customMessage": null, - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 1, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Completed", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:16:39.019Z", - "updatedAt": "2021-05-09T21:16:39.019Z" + "updatedAt": "2021-05-09T21:16:39.019Z", + "deletedAt": null } ] }, @@ -254,54 +291,81 @@ "interviews": [ { "id": "976d23a9-5710-453f-99d9-f57a588bb610", + "xaiId": null, "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 3, "startTimestamp": null, - "attendeesList": [ - "attendee1@yopmail.com", - "attendee2@yopmail.com" - ], + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Scheduling", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:21:28.713Z", - "updatedAt": "2021-05-09T21:21:28.713Z" + "updatedAt": "2021-05-09T21:21:28.713Z", + "deletedAt": null }, { "id": "a23e1bf2-1084-4cfe-a0d8-d83bc6fec655", + "xaiId": null, "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", - "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 2, "startTimestamp": null, - "attendeesList": [ - "attendee1@yopmail.com", - "attendee2@yopmail.com" - ], + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Scheduling", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:21:22.428Z", - "updatedAt": "2021-05-09T21:21:22.428Z" + "updatedAt": "2021-05-09T21:21:22.428Z", + "deletedAt": null }, { "id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb", + "xaiId": null, "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": null, - "customMessage": null, - "xaiTemplate": "interview-30", + "calendarEventId": null, + "templateUrl": "interview-30", + "templateId": null, + "templateType": null, + "title": null, + "locationDetails": null, + "duration": null, "round": 1, "startTimestamp": null, - "attendeesList": null, + "endTimestamp": null, + "hostName": null, + "hostEmail": null, + "guestNames": null, + "guestEmails": null, "status": "Completed", + "rescheduleUrl": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, "createdAt": "2021-05-09T21:21:17.346Z", - "updatedAt": "2021-05-09T21:21:17.346Z" + "updatedAt": "2021-05-09T21:21:17.346Z", + "deletedAt": null } ] }, @@ -2052,5 +2116,127 @@ } ] } + ], + "Role": [ + { + "id": "c145247d-5757-463d-9317-ff9e7026d403", + "name": "Angular Developer", + "description": "Angular is an open-source, client-side framework based on TypeScript and designed for building web applications.", + "listOfSkills": [ + "database", + "winforms", + "user interface (ui)", + "photoshop" + ], + "rates": [ + { + "global": 50, + "offShore": 10, + "inCountry": 20 + }, + { + "global": 25, + "offShore": 5, + "inCountry": 15 + } + ], + "numberOfMembers": "10", + "numberOfMembersAvailable": 8, + "imageUrl": "http://images.topcoder.com/member", + "timeToCandidate": 105, + "timeToInterview": 100, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-27T21:43:08.201Z", + "updatedAt": "2021-05-27T21:43:08.201Z" + }, + { + "id": "d7ff0289-d3ea-44d8-b39a-53bba5b5b309", + "name": "Dev Ops Engineer", + "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.", + "listOfSkills": [ + "dropwizard", + "nginx", + "machine learning", + "force.com" + ], + "rates": [ + { + "global": 50, + "offShore": 10, + "inCountry": 20, + "rate20Global": 20, + "rate30Global": 20, + "rate20OffShore": 35, + "rate30OffShore": 35, + "rate20InCountry": 15, + "rate30InCountry": 15 + }, + { + "global": 25, + "offShore": 5, + "inCountry": 15, + "rate20Global": 20, + "rate30Global": 20, + "rate20OffShore": 35, + "rate30OffShore": 35, + "rate20InCountry": 15, + "rate30InCountry": 15 + } + ], + "numberOfMembers": "10", + "numberOfMembersAvailable": 8, + "imageUrl": "http://images.topcoder.com/member", + "timeToCandidate": 105, + "timeToInterview": 100, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-27T21:43:04.717Z", + "updatedAt": "2021-05-27T21:43:04.717Z" + }, + { + "id": "e7b7e818-40d4-4102-b486-09bdd21400b8", + "name": "Salesforce Developer", + "description": "A Salesforce developer is a programmer who builds Salesforce applications across various PaaS (Platform as a Service) platforms.", + "listOfSkills": [ + "docker", + ".net", + "appcelerator", + "flux" + ], + "rates": [ + { + "global": 50, + "offShore": 10, + "inCountry": 20, + "rate20Global": 20, + "rate30Global": 20, + "rate20OffShore": 35, + "rate30OffShore": 35, + "rate20InCountry": 15, + "rate30InCountry": 15 + }, + { + "global": 25, + "offShore": 5, + "inCountry": 15, + "rate20Global": 20, + "rate30Global": 20, + "rate20OffShore": 35, + "rate30OffShore": 35, + "rate20InCountry": 15, + "rate30InCountry": 15 + } + ], + "numberOfMembers": "10", + "numberOfMembersAvailable": 6, + "imageUrl": "http://images.topcoder.com/member", + "timeToCandidate": 105, + "timeToInterview": 100, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": null, + "createdAt": "2021-05-27T21:43:09.342Z", + "updatedAt": "2021-05-27T21:43:09.342Z" + } ] -} +} \ No newline at end of file diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index a0518c50..39e8c0cb 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", + "_postman_id": "82ad9051-0f67-46dd-8875-cce9979a22f4", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -17816,6 +17816,2970 @@ } ] }, + { + "name": "Roles", + "item": [ + { + "name": "Create Role", + "item": [ + { + "name": "create role with admin", + "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(\"roleId-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 ],\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}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with booking manager", + "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(\"roleId-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 \"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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\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}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with m2m create", + "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(\"roleId-3\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_create_role}}" + } + ], + "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 ],\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\": 6,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role 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", + "type": "text", + "value": "Bearer invalid_token" + } + ], + "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with existent name", + "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(\"Role: \\\"Dev Ops Engineer\\\" is already exists.\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with missing parameter 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(\"\\\"role.name\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with missing parameter 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(\"\\\"role.rates\\\" is required\")\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 ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with missing parameter 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(\"\\\"role.rates\\\" does not contain 1 required value(s)\")\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 ],\n \"rates\": [\n\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with missing parameter 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(\"\\\"role.rates[0].global\\\" is required\")\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 ],\n \"rates\": [\n {\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with missing parameter 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(\"\\\"role.rates[0].inCountry\\\" is required\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with missing parameter 6", + "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(\"\\\"role.rates[0].offShore\\\" is required\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 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(\"\\\"role.name\\\" length must be less than or equal to 50 characters long\")\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 Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer 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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 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(\"\\\"role.listOfSkills[0]\\\" length must be less than or equal to 50 characters long\")\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 Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard\",\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 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(\"\\\"role.listOfSkills\\\" must be an array\")\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\":\"Dropwizard\",\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 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(\"\\\"role.rates\\\" must be an array\")\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 ],\n \"rates\": {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 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(\"\\\"role.rates[0].global\\\" must be a number\")\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 ],\n \"rates\": [\n {\n \"global\": \"first\",\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 6", + "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(\"\\\"role.rates[0].inCountry\\\" must be a number\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": \"fifty\",\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 7", + "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(\"\\\"role.numberOfMembers\\\" must be a number\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": null,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 8", + "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(\"\\\"role.imageUrl\\\" must be a valid uri\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 9", + "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(\"\\\"role.timeToCandidate\\\" must be less than or equal to 32767\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 99999,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "create role with invalid parameter 10", + "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(\"skills: \\\"teamworking,communication,problem-solving\\\" are not valid\")\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 2\",\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 \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 55,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Get Role", + "item": [ + { + "name": "get role with admin", + "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", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "get role with booking manager 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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-2}}?fromDb=true", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-2}}" + ], + "query": [ + { + "key": "fromDb", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "get role with m2m read", + "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", + "type": "text", + "value": "Bearer {{token_m2m_read_role}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-3}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-3}}" + ] + } + }, + "response": [] + }, + { + "name": "get role with connect user 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", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}?fromDb=true", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ], + "query": [ + { + "key": "fromDb", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "get role with member", + "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", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-2}}" + ] + } + }, + "response": [] + }, + { + "name": "get role 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": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer invalid token" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-2}}" + ] + } + }, + "response": [] + }, + { + "name": "get role with invalid id", + "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(\"\\\"id\\\" must be a valid GUID\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "url": { + "raw": "{{URL}}/roles/invalid", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "invalid" + ] + } + }, + "response": [] + }, + { + "name": "get role with missing id", + "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\\\" not found\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "url": { + "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "00000000-0000-0000-0000-000000000000" + ] + } + }, + "response": [] + }, + { + "name": "search roles with admin", + "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", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "search roles with booking manager", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/roles?skillsList=dropwizard, nginx,, machine learning , FORce.com &keyword=ops e", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ], + "query": [ + { + "key": "skillsList", + "value": "dropwizard, nginx,, machine learning , FORce.com " + }, + { + "key": "keyword", + "value": "ops e" + } + ] + } + }, + "response": [] + }, + { + "name": "search roles with connect user", + "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", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "url": { + "raw": "{{URL}}/roles?skillsList=dataBase, ,Photoshop&keyword=sale", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ], + "query": [ + { + "key": "skillsList", + "value": "dataBase, ,Photoshop" + }, + { + "key": "keyword", + "value": "sale" + } + ] + } + }, + "response": [] + }, + { + "name": "search roles with m2m read", + "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", + "type": "text", + "value": "Bearer {{token_m2m_read_role}}" + } + ], + "url": { + "raw": "{{URL}}/roles?skillsList=DOCKER,.NET&keyword=dev", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ], + "query": [ + { + "key": "skillsList", + "value": "DOCKER,.NET" + }, + { + "key": "keyword", + "value": "dev" + } + ] + } + }, + "response": [] + }, + { + "name": "search roles with member", + "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", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "url": { + "raw": "{{URL}}/roles?keyword=dev", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ], + "query": [ + { + "key": "keyword", + "value": "dev" + } + ] + } + }, + "response": [] + }, + { + "name": "search roles 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": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer invalid token" + } + ], + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Update Role", + "item": [ + { + "name": "update role with admin", + "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", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer edit\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with booking manager", + "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", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Angular Developer edit\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-2}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with m2m update", + "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", + "type": "text", + "value": "Bearer {{token_m2m_update_role}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Salesforce Developer edit\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-3}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-3}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role 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": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer invalid_token" + } + ], + "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid id", + "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(\"\\\"id\\\" must be a valid GUID\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/invalid", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "invalid" + ] + } + }, + "response": [] + }, + { + "name": "update role with missing id", + "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": "PATCH", + "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "00000000-0000-0000-0000-000000000000" + ] + } + }, + "response": [] + }, + { + "name": "update role with existent name", + "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(\"Role: \\\"Angular Developer edit\\\" is already exists.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Angular Developer edit\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 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.name\\\" length must be less than or equal to 50 characters long\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer 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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 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.listOfSkills[0]\\\" length must be less than or equal to 50 characters long\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "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 \"Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking\",\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 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.listOfSkills\\\" must be an array\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "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\":\"Teamworking\",\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 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.rates\\\" must be an array\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 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.rates[0].global\\\" must be a number\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": \"first\",\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 6", + "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.rates[0].inCountry\\\" must be a number\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": \"fifty\",\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 7", + "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.numberOfMembers\\\" must be a number\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": \"hundred\",\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 8", + "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.imageUrl\\\" must be a valid uri\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 9", + "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.timeToCandidate\\\" must be less than or equal to 32767\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 99999,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "update role with invalid parameter 10", + "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(\"skills: \\\"teamworking\\\" are not valid\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "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 \"Teamworking\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 66,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Delete Role", + "item": [ + { + "name": "delete role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "delete role with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "delete role 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": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer invalid_token" + } + ], + "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 \"teamworking\",\n \"communication\",\n \"problem-solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "delete role with invalid id", + "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(\"\\\"id\\\" must be a valid GUID\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/invalid", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "invalid" + ] + } + }, + "response": [] + }, + { + "name": "delete role with missing id", + "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": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "00000000-0000-0000-0000-000000000000" + ] + } + }, + "response": [] + }, + { + "name": "delete role with admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204', function () {\r", + " pm.response.to.have.status(204);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "delete role with booking manager", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204', function () {\r", + " pm.response.to.have.status(204);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-2}}" + ] + } + }, + "response": [] + }, + { + "name": "delete role with m2m delete", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204', function () {\r", + " pm.response.to.have.status(204);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_delete_role}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-3}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-3}}" + ] + } + }, + "response": [] + } + ] + } + ] + }, { "name": "health check", "item": [ @@ -22399,7 +25363,226 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_by_administrator}}", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments", + "{{workPeriodPaymentId_created_by_administrator}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Roles", + "item": [ + { + "name": "✔ create role with admin", + "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(\"roleId-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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "✔ get role with admin", + "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", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "✔ search roles with admin", + "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", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "✔ update role with admin", + "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", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dev Ops Engineer edit\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "✔ delete role with admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204', function () {\r", + " pm.response.to.have.status(204);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "", "options": { "raw": { "language": "json" @@ -22407,13 +25590,13 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_by_administrator}}", + "raw": "{{URL}}/roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "work-period-payments", - "{{workPeriodPaymentId_created_by_administrator}}" + "roles", + "{{roleId-1}}" ] } }, @@ -24635,12 +27818,293 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_member_tester1234}}" + "value": "Bearer {{token_member_tester1234}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_for_member}}", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments", + "{{workPeriodPaymentId_created_for_member}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Roles", + "item": [ + { + "name": "Before Start", + "item": [ + { + "name": "✔ create role with admin", + "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(\"roleId-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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "✘ create role with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "✔ get role with member", + "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", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "✔ search roles with member", + "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", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "url": { + "raw": "{{URL}}/roles?keyword=Dev", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ], + "query": [ + { + "key": "keyword", + "value": "Dev" + } + ] + } + }, + "response": [] + }, + { + "name": "✘ update role with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "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 \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "✘ delete role with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", + "raw": "", "options": { "raw": { "language": "json" @@ -24648,13 +28112,13 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_for_member}}", + "raw": "{{URL}}/roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "work-period-payments", - "{{workPeriodPaymentId_created_for_member}}" + "roles", + "{{roleId-1}}" ] } }, @@ -26894,10 +30358,295 @@ "response": [] } ] + }, + { + "name": "Roles", + "item": [ + { + "name": "Before Start", + "item": [ + { + "name": "✔ create role with admin", + "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(\"roleId-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 2\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "✘ create role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ] + } + }, + "response": [] + }, + { + "name": "✔ get role with connect user", + "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", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "✔ search roles with connect user", + "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", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "url": { + "raw": "{{URL}}/roles?skillsList=Dropwizard, ,NGINX&keyword=Dev", + "host": [ + "{{URL}}" + ], + "path": [ + "roles" + ], + "query": [ + { + "key": "skillsList", + "value": "Dropwizard, ,NGINX" + }, + { + "key": "keyword", + "value": "Dev" + } + ] + } + }, + "response": [] + }, + { + "name": "✘ update role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "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 \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + }, + { + "name": "✘ delete role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connectUser}}" + } + ], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "roles", + "{{roleId-1}}" + ] + } + }, + "response": [] + } + ] } ] } ] } ] -} +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a0b6064b..e5f1ac26 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -18,6 +18,8 @@ tags: - name: ResourceBookings - name: Teams - name: WorkPeriods + - name: WorkPeriodPayments + - name: Roles paths: /jobs: post: @@ -3245,6 +3247,267 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /roles/new: + post: + tags: + - Roles + description: | + Create Role. + + **Authorization** Topcoder m2m token with create scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RoleRequestBody" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Role" + "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" + /roles: + get: + tags: + - Roles + description: | + Search roles. + + **Authorization** Topcoder m2m token with read scope is allowed. Topcoder user token with any role is allowed. + security: + - bearerAuth: [] + parameters: + - in: query + name: skillsList + required: false + schema: + type: string + description: comma separated skill names. case-insensitive. + - in: query + name: keyword + required: false + schema: + type: string + description: role name. case-insensitive. partial match allowed + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Role" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /roles/{id}: + get: + tags: + - Roles + description: | + Get role by id. + + **Authorization** Topcoder m2m token with read scope is allowed. Topcoder user token with any role is allowed. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + description: The role id. + required: true + schema: + type: string + format: uuid + - in: query + name: fromDb + description: get data from db or not. + required: false + schema: + type: boolean + default: false + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Role" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + 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" + patch: + tags: + - Roles + description: | + Partial Update role. + + **Authorization** Topcoder m2m token with update scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + description: The id of role. + required: true + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RolePatchRequestBody" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Role" + "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" + delete: + tags: + - Roles + description: | + Delete the role. + + **Authorization** Topcoder m2m token with delete scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + description: The id of role. + required: true + schema: + type: string + format: uuid + responses: + "204": + description: OK + "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" /health: get: tags: @@ -3335,6 +3598,13 @@ components: type: string format: uuid description: "The skill id." + roleIds: + type: array + description: "The roles." + items: + type: string + format: uuid + description: "The role id." status: type: string enum: ["sourcing", "in-review", "assigned", "closed", "cancelled"] @@ -3424,6 +3694,13 @@ components: type: string format: uuid description: "The skill id." + roleIds: + type: array + description: "The roles." + items: + type: string + format: uuid + description: "The role id." isApplicationPageActive: type: boolean default: false @@ -3865,6 +4142,13 @@ components: type: string format: uuid description: "The skill id." + roleIds: + type: array + description: "The roles." + items: + type: string + format: uuid + description: "The role id." isApplicationPageActive: type: boolean default: false @@ -4710,6 +4994,198 @@ components: type: string description: "the email of a member" example: "xxx@xxx.com" + Role: + required: + - id + - name + - rates + - createdAt + - createdBy + properties: + id: + type: string + format: uuid + description: "The role id." + name: + type: string + example: "Dev Ops Engineer" + description: "The role name." + description: + type: string + example: "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." + description: "The role description" + listOfSkills: + type: array + description: "The array of skill names." + items: + type: string + example: "HTML" + description: "The skill name" + rates: + type: array + description: "The rates object array." + items: + $ref: "#/components/schemas/RoleRates" + numberOfMembers: + type: number + example: 100 + description: "The number of members." + numberOfMembersAvailable: + type: integer + example: 100 + description: "The number of members available." + imageUrl: + type: string + format: url + example: "http://images.topcoder.com/images" + description: "The image url of the role." + timeToCandidate: + type: integer + example: 200 + description: "The time to candidate." + timeToInterview: + type: integer + example: 300 + description: "The time to interview." + createdAt: + type: string + format: date-time + description: "The role created date." + createdBy: + type: string + format: uuid + description: "The user Id who created the role.(Will get the user info from the token)" + updatedAt: + type: string + format: date-time + description: "The role last updated at." + updatedBy: + type: string + format: uuid + description: "The user Id who updated the role last time.(Will get the user info from the token)" + RoleRequestBody: + required: + - name + - rates + properties: + name: + type: string + example: "Dev Ops Engineer" + description: "The role name." + description: + type: string + example: "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." + description: "The role description" + listOfSkills: + type: array + description: "The array of skill names." + items: + type: string + example: "HTML" + description: "The skill name" + rates: + type: array + description: "The rates object array." + items: + $ref: "#/components/schemas/RoleRates" + numberOfMembers: + type: number + example: 100 + description: "The number of members." + numberOfMembersAvailable: + type: number + example: 100 + description: "The number of members available." + imageUrl: + type: string + format: url + example: "http://images.topcoder.com/images" + description: "The image url of the role." + timeToCandidate: + type: integer + example: 200 + description: "The time to candidate." + timeToInterview: + type: integer + example: 300 + description: "The time to interview." + RolePatchRequestBody: + properties: + name: + type: string + example: "Dev Ops Engineer" + description: "The role name." + description: + type: string + example: "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." + description: "The role description" + listOfSkills: + type: array + description: "The array of skill names." + items: + type: string + example: "HTML" + description: "The skill name" + rates: + type: array + description: "The rates object array." + items: + $ref: "#/components/schemas/RoleRates" + numberOfMembers: + type: number + example: 100 + description: "The number of members." + numberOfMembersAvailable: + type: number + example: 100 + description: "The number of members available." + imageUrl: + type: string + format: url + example: "http://images.topcoder.com/images" + description: "The image url of the role." + timeToCandidate: + type: integer + example: 200 + description: "The time to candidate." + timeToInterview: + type: integer + example: 300 + description: "The time to interview." + RoleRates: + required: + - global + - inCountry + - offShore + type: object + properties: + global: + type: integer + example: 10 + inCountry: + type: integer + example: 20 + offShore: + type: integer + example: 30 + rate30Global: + type: integer + example: 10 + rate30InCountry: + type: integer + example: 20 + rate30OffShore: + type: integer + example: 30 + rate20Global: + type: integer + example: 10 + rate20InCountry: + type: integer + example: 20 + rate20OffShore: + type: integer + example: 30 ProjectMember: type: object example: diff --git a/docs/topcoder-bookings.postman_environment.json b/docs/topcoder-bookings.postman_environment.json index 837b55db..c83fc9af 100644 --- a/docs/topcoder-bookings.postman_environment.json +++ b/docs/topcoder-bookings.postman_environment.json @@ -1,5 +1,5 @@ { - "id": "228f4dcc-6914-462e-9b56-3285b643a2f8", + "id": "0ce42def-1c70-4c24-8986-914caa57f3c8", "name": "topcoder-bookings", "values": [ { @@ -312,11 +312,6 @@ "value": "", "enabled": true }, - { - "key": "job_id_created_for_member", - "value": "", - "enabled": true - }, { "key": "resource_bookings_id_created_for_member", "value": "", @@ -327,11 +322,6 @@ "value": "", "enabled": true }, - { - "key": "job_id_created_for_connect_manager", - "value": "", - "enabled": true - }, { "key": "resource_bookings_id_created_for_connect_manager", "value": "", @@ -461,9 +451,49 @@ "key": "interview_id_created_for_connect_manager", "value": "", "enabled": true + }, + { + "key": "token_m2m_create_role", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.f1QP1QTacyDxy7dwzUhBIT8blXCjKn_mnu9Cg59vIc8", + "enabled": true + }, + { + "key": "token_m2m_read_role", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJyZWFkOnRhYXMtcm9sZXMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.ZeWS_W2o8YwlvIB_-z0CFFa9zhRjptCk7qNXsPPWxVY", + "enabled": true + }, + { + "key": "token_m2m_update_role", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJ1cGRhdGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.0t4k0skZmxAUKuHQrG3ZrO2dgWcDMLD8W1rVluCy7XQ", + "enabled": true + }, + { + "key": "token_m2m_delete_role", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJkZWxldGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.NSBbWOk5jCB8nIvLiZwJtR9px5wmUQaQjgpDlMDJ9hk", + "enabled": true + }, + { + "key": "token_m2m_all_role", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJhbGw6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.cn0QVTOFnbHJckYqmGcpUBT8wQUxXWwtteWU7uhlDtI", + "enabled": true + }, + { + "key": "roleId-1", + "value": "", + "enabled": true + }, + { + "key": "roleId-2", + "value": "", + "enabled": true + }, + { + "key": "roleId-3", + "value": "", + "enabled": true } ], "_postman_variable_scope": "environment", - "_postman_exported_at": "2021-05-10T05:06:38.661Z", - "_postman_exported_using": "Postman/8.3.1" + "_postman_exported_at": "2021-05-27T01:32:45.726Z", + "_postman_exported_using": "Postman/8.5.1" } \ No newline at end of file diff --git a/local/kafka-client/topics.txt b/local/kafka-client/topics.txt index 8766a1b5..760c3a82 100644 --- a/local/kafka-client/topics.txt +++ b/local/kafka-client/topics.txt @@ -3,16 +3,19 @@ taas.jobcandidate.create taas.resourcebooking.create taas.workperiod.create taas.workperiodpayment.create +taas.role.requested taas.job.update taas.jobcandidate.update taas.resourcebooking.update taas.workperiod.update taas.workperiodpayment.update +taas.role.update taas.job.delete taas.jobcandidate.delete taas.resourcebooking.delete taas.workperiod.delete taas.workperiodpayment.delete +taas.role.delete taas.interview.requested taas.interview.update taas.interview.bulkUpdate diff --git a/migrations/2021-05-27-1-role-table-create.js b/migrations/2021-05-27-1-role-table-create.js new file mode 100644 index 00000000..bce2ae17 --- /dev/null +++ b/migrations/2021-05-27-1-role-table-create.js @@ -0,0 +1,146 @@ +const config = require('config') + +/* + * Create role table + */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.createTable('roles', { + id: { + type: Sequelize.UUID, + primaryKey: true, + allowNull: false, + defaultValue: Sequelize.UUIDV4 + }, + name: { + type: Sequelize.STRING(50), + allowNull: false + }, + description: { + type: Sequelize.STRING(1000) + }, + listOfSkills: { + field: 'list_of_skills', + type: Sequelize.ARRAY({ + type: Sequelize.STRING(50) + }) + }, + rates: { + type: Sequelize.ARRAY({ + type: Sequelize.JSONB({ + global: { + type: Sequelize.SMALLINT, + allowNull: false + }, + inCountry: { + field: 'in_country', + type: Sequelize.SMALLINT, + allowNull: false + }, + offShore: { + field: 'off_shore', + type: Sequelize.SMALLINT, + allowNull: false + }, + rate30Global: { + field: 'rate30_global', + type: Sequelize.SMALLINT + }, + rate30InCountry: { + field: 'rate30_in_country', + type: Sequelize.SMALLINT + }, + rate30OffShore: { + field: 'rate30_off_shore', + type: Sequelize.SMALLINT + }, + rate20Global: { + field: 'rate20_global', + type: Sequelize.SMALLINT + }, + rate20InCountry: { + field: 'rate20_in_country', + type: Sequelize.SMALLINT + }, + rate20OffShore: { + field: 'rate20_off_shore', + type: Sequelize.SMALLINT + } + }), + allowNull: false + }), + allowNull: false + }, + numberOfMembers: { + field: 'number_of_members', + type: Sequelize.NUMERIC + }, + numberOfMembersAvailable: { + field: 'number_of_members_available', + type: Sequelize.SMALLINT + }, + imageUrl: { + field: 'image_url', + type: Sequelize.STRING(255) + }, + timeToCandidate: { + field: 'time_to_candidate', + type: Sequelize.SMALLINT + }, + timeToInterview: { + field: 'time_to_interview', + type: Sequelize.SMALLINT + }, + 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, + transaction + }) + await queryInterface.addIndex( + { + tableName: 'roles', + schema: config.DB_SCHEMA_NAME + }, + ['name'], + { + type: 'UNIQUE', + where: { deleted_at: null }, + transaction: transaction + } + ) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable({ + tableName: 'roles', + schema: config.DB_SCHEMA_NAME + }) + } +} diff --git a/migrations/2021-05-27-2-job-add-roleIds-field.js b/migrations/2021-05-27-2-job-add-roleIds-field.js new file mode 100644 index 00000000..a5b9f4be --- /dev/null +++ b/migrations/2021-05-27-2-job-add-roleIds-field.js @@ -0,0 +1,19 @@ +const config = require('config') + +/* + * Add roleIds field to the Job model. + */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'role_ids', + { + type: Sequelize.ARRAY({ + type: Sequelize.UUID + }) + }) + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'role_ids') + } +} diff --git a/package.json b/package.json index 0fa24cca..510504f1 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "index:jobs": "node scripts/es/reIndexJobs.js", "index:job-candidates": "node scripts/es/reIndexJobCandidates.js", "index:resource-bookings": "node scripts/es/reIndexResourceBookings.js", + "index:roles": "node scripts/es/reIndexRoles.js", "data:export": "node scripts/data/exportData.js", "data:import": "node scripts/data/importData.js", "migrate": "npx sequelize db:migrate", diff --git a/scripts/data/exportData.js b/scripts/data/exportData.js index 4eee1ad5..cb61e582 100644 --- a/scripts/data/exportData.js +++ b/scripts/data/exportData.js @@ -28,7 +28,7 @@ const resourceBookingModelOpts = { const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH const userPrompt = `WARNING: are you sure you want to export all data in the database to a json file with the path ${filePath}? This will overwrite the file.` -const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts] +const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts, 'Role'] async function exportData () { await helper.promptUser(userPrompt, async () => { diff --git a/scripts/data/importData.js b/scripts/data/importData.js index 2e9c168e..a0aeeb64 100644 --- a/scripts/data/importData.js +++ b/scripts/data/importData.js @@ -28,7 +28,7 @@ const resourceBookingModelOpts = { const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH const userPrompt = `WARNING: this would remove existing data. Are you sure you want to import data from a json file with the path ${filePath}?` -const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts] +const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts, 'Role'] async function importData () { await helper.promptUser(userPrompt, async () => { diff --git a/scripts/es/createIndex.js b/scripts/es/createIndex.js index d2c72943..269cd5a4 100644 --- a/scripts/es/createIndex.js +++ b/scripts/es/createIndex.js @@ -8,7 +8,8 @@ const helper = require('../../src/common/helper') const indices = [ config.get('esConfig.ES_INDEX_JOB'), config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + config.get('esConfig.ES_INDEX_ROLE') ] const userPrompt = `WARNING: Are you sure want to create the following elasticsearch indices: ${indices}?` diff --git a/scripts/es/deleteIndex.js b/scripts/es/deleteIndex.js index 6e30995a..724d3556 100644 --- a/scripts/es/deleteIndex.js +++ b/scripts/es/deleteIndex.js @@ -8,7 +8,8 @@ const helper = require('../../src/common/helper') const indices = [ config.get('esConfig.ES_INDEX_JOB'), config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + config.get('esConfig.ES_INDEX_ROLE') ] const userPrompt = `WARNING: this would remove existent data! Are you sure want to delete the following eleasticsearch indices: ${indices}?` diff --git a/scripts/es/reIndexAll.js b/scripts/es/reIndexAll.js index 802695dd..0367be11 100644 --- a/scripts/es/reIndexAll.js +++ b/scripts/es/reIndexAll.js @@ -34,6 +34,7 @@ async function indexAll () { await helper.indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) await helper.indexBulkDataToES(jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger) await helper.indexBulkDataToES(resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger) + await helper.indexBulkDataToES('Role', config.get('esConfig.ES_INDEX_ROLE'), logger) process.exit(0) } catch (err) { logger.logFullError(err, { component: 'indexAll' }) diff --git a/scripts/es/reIndexRoles.js b/scripts/es/reIndexRoles.js new file mode 100644 index 00000000..a4507aa9 --- /dev/null +++ b/scripts/es/reIndexRoles.js @@ -0,0 +1,37 @@ +/** + * Reindex Roles data in Elasticsearch using data from database + */ +const config = require('config') +const logger = require('../../src/common/logger') +const helper = require('../../src/common/helper') + +const roleId = helper.getParamFromCliArgs() +const index = config.get('esConfig.ES_INDEX_ROLE') +const reIndexAllRolesPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the index ${index}?` +const reIndexRolePrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the document with id ${roleId} in index ${index}?` + +async function reIndexRoles () { + if (roleId === null) { + await helper.promptUser(reIndexAllRolesPrompt, async () => { + try { + await helper.indexBulkDataToES('Role', index, logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'reIndexRoles' }) + process.exit(1) + } + }) + } else { + await helper.promptUser(reIndexRolePrompt, async () => { + try { + await helper.indexDataToEsById(roleId, 'Role', index, logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'reIndexRoles' }) + process.exit(1) + } + }) + } +} + +reIndexRoles() diff --git a/src/bootstrap.js b/src/bootstrap.js index 2999f131..896e6c9c 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -16,7 +16,7 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') -Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed') +Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed') Joi.title = () => Joi.string().max(128) Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) @@ -26,6 +26,7 @@ Joi.workPeriodPaymentStatus = () => Joi.string().valid('completed', 'cancelled') // See https://joi.dev/api/?v=17.3.0#string fro details why it's like this. // In many cases we would like to allow empty string to make it easier to create UI for editing data. Joi.stringAllowEmpty = () => Joi.string().allow('') +Joi.smallint = () => Joi.number().min(-32768).max(32767) function buildServices (dir) { const files = fs.readdirSync(dir) diff --git a/src/common/helper.js b/src/common/helper.js index 0ce11905..66cf32d1 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -2,50 +2,50 @@ * This file defines helper methods */ -const fs = require('fs'); -const querystring = require('querystring'); -const Confirm = require('prompt-confirm'); -const Bottleneck = require('bottleneck'); -const AWS = require('aws-sdk'); -const config = require('config'); -const HttpStatus = require('http-status-codes'); -const _ = require('lodash'); -const request = require('superagent'); -const elasticsearch = require('@elastic/elasticsearch'); +const fs = require('fs') +const querystring = require('querystring') +const Confirm = require('prompt-confirm') +const Bottleneck = require('bottleneck') +const AWS = require('aws-sdk') +const config = require('config') +const HttpStatus = require('http-status-codes') +const _ = require('lodash') +const request = require('superagent') +const elasticsearch = require('@elastic/elasticsearch') const { - ResponseError: ESResponseError, -} = require('@elastic/elasticsearch/lib/errors'); -const errors = require('../common/errors'); -const logger = require('./logger'); -const models = require('../models'); -const eventDispatcher = require('./eventDispatcher'); -const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper'); -const moment = require('moment'); + ResponseError: ESResponseError +} = require('@elastic/elasticsearch/lib/errors') +const errors = require('../common/errors') +const logger = require('./logger') +const models = require('../models') +const eventDispatcher = require('./eventDispatcher') +const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') +const moment = require('moment') const localLogger = { debug: (message) => logger.debug({ component: 'helper', context: message.context, - message: message.message, + message: message.message }), error: (message) => logger.error({ component: 'helper', context: message.context, - message: message.message, + message: message.message }), info: (message) => logger.info({ component: 'helper', context: message.context, - message: message.message, - }), -}; + message: message.message + }) +} -AWS.config.region = config.esConfig.AWS_REGION; +AWS.config.region = config.esConfig.AWS_REGION -const m2mAuth = require('tc-core-library-js').auth.m2m; +const m2mAuth = require('tc-core-library-js').auth.m2m const m2m = m2mAuth( _.pick(config, [ @@ -53,9 +53,9 @@ const m2m = m2mAuth( 'AUTH0_AUDIENCE', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', - 'AUTH0_PROXY_SERVER_URL', + 'AUTH0_PROXY_SERVER_URL' ]) -); +) const m2mForUbahn = m2mAuth({ AUTH0_AUDIENCE: config.AUTH0_AUDIENCE_UBAHN, @@ -64,20 +64,20 @@ const m2mForUbahn = m2mAuth({ 'TOKEN_CACHE_TIME', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', - 'AUTH0_PROXY_SERVER_URL', - ]), -}); + 'AUTH0_PROXY_SERVER_URL' + ]) +}) -let busApiClient; +let busApiClient /** * Get bus api client. * * @returns {Object} the bus api client */ -function getBusApiClient() { +function getBusApiClient () { if (busApiClient) { - return busApiClient; + return busApiClient } busApiClient = busApi( _.pick(config, [ @@ -88,17 +88,17 @@ function getBusApiClient() { 'AUTH0_CLIENT_SECRET', 'BUSAPI_URL', 'KAFKA_ERROR_TOPIC', - 'AUTH0_PROXY_SERVER_URL', + 'AUTH0_PROXY_SERVER_URL' ]) - ); - return busApiClient; + ) + return busApiClient } // ES Client mapping -const esClients = {}; +const esClients = {} // The es index property mapping -const esIndexPropertyMapping = {}; +const esIndexPropertyMapping = {} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { projectId: { type: 'integer' }, externalId: { type: 'keyword' }, @@ -113,11 +113,12 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { skills: { type: 'keyword' }, status: { type: 'keyword' }, isApplicationPageActive: { type: 'boolean' }, + roleIds: { type: 'keyword' }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { jobId: { type: 'keyword' }, userId: { type: 'keyword' }, @@ -150,14 +151,14 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, updatedBy: { type: 'keyword' }, - deletedAt: { type: 'date' }, - }, + deletedAt: { type: 'date' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { projectId: { type: 'integer' }, userId: { type: 'keyword' }, @@ -195,32 +196,59 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, - }, + updatedBy: { type: 'keyword' } + } }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, - }, + updatedBy: { type: 'keyword' } + } + }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, + updatedBy: { type: 'keyword' } +} +esIndexPropertyMapping[config.get('esConfig.ES_INDEX_ROLE')] = { + name: { type: 'keyword' }, + description: { type: 'keyword' }, + listOfSkills: { type: 'keyword' }, + rates: { + properties: { + global: { type: 'integer' }, + inCountry: { type: 'integer' }, + offShore: { type: 'integer' }, + rate30Global: { type: 'integer' }, + rate30InCountry: { type: 'integer' }, + rate30OffShore: { type: 'integer' }, + rate20Global: { type: 'integer' }, + rate20InCountry: { type: 'integer' }, + rate20OffShore: { type: 'integer' } + } }, + numberOfMembers: { type: 'integer' }, + numberOfMembersAvailable: { type: 'integer' }, + imageUrl: { type: 'keyword' }, + timeToCandidate: { type: 'integer' }, + timeToInterview: { type: 'integer' }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -}; + updatedBy: { type: 'keyword' } +} /** * Get the first parameter from cli arguments */ -function getParamFromCliArgs() { - const filteredArgs = process.argv.filter((arg) => !arg.includes('--')); +function getParamFromCliArgs () { + const filteredArgs = process.argv.filter((arg) => !arg.includes('--')) if (filteredArgs.length > 2) { - return filteredArgs[2]; + return filteredArgs[2] } - return null; + return null } /** @@ -228,18 +256,18 @@ function getParamFromCliArgs() { * @param {string} promptQuery the query to ask the user * @param {function} cb the callback function */ -async function promptUser(promptQuery, cb) { +async function promptUser (promptQuery, cb) { if (process.argv.includes('--force')) { - await cb(); - return; + await cb() + return } - const prompt = new Confirm(promptQuery); + const prompt = new Confirm(promptQuery) prompt.ask(async (answer) => { if (answer) { - await cb(); + await cb() } - }); + }) } /** @@ -248,23 +276,23 @@ async function promptUser(promptQuery, cb) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function createIndex(index, logger, esClient = null) { +async function createIndex (index, logger, esClient = null) { if (!esClient) { - esClient = getESClient(); + esClient = getESClient() } await esClient.indices.create({ index, body: { mappings: { - properties: esIndexPropertyMapping[index], - }, - }, - }); + properties: esIndexPropertyMapping[index] + } + } + }) logger.info({ component: 'createIndex', - message: `ES Index ${index} creation succeeded!`, - }); + message: `ES Index ${index} creation succeeded!` + }) } /** @@ -273,45 +301,45 @@ async function createIndex(index, logger, esClient = null) { * @param {Object} logger the logger object * @param {Object} esClient the elasticsearch client (optional, will create if not given) */ -async function deleteIndex(index, logger, esClient = null) { +async function deleteIndex (index, logger, esClient = null) { if (!esClient) { - esClient = getESClient(); + esClient = getESClient() } - await esClient.indices.delete({ index }); + await esClient.indices.delete({ index }) logger.info({ component: 'deleteIndex', - message: `ES Index ${index} deletion succeeded!`, - }); + message: `ES Index ${index} deletion succeeded!` + }) } /** * Split data into bulks * @param {Array} data the array of data to split */ -function getBulksFromDocuments(data) { - const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6; - const bulks = []; - let documentIndex = 0; - let currentBulkSize = 0; - let currentBulk = []; +function getBulksFromDocuments (data) { + const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6 + const bulks = [] + let documentIndex = 0 + let currentBulkSize = 0 + let currentBulk = [] while (true) { // break loop when parsed all documents if (documentIndex >= data.length) { - bulks.push(currentBulk); - break; + bulks.push(currentBulk) + break } // check if current document size is greater than the max bulk size, if so, throw error const currentDocumentSize = Buffer.byteLength( JSON.stringify(data[documentIndex]), 'utf-8' - ); + ) if (maxBytes < currentDocumentSize) { throw new Error( `Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.` - ); + ) } if ( @@ -320,17 +348,17 @@ function getBulksFromDocuments(data) { ) { // if adding the current document goes over the max bulk size OR goes over max number of docs // then push the current bulk to bulks array and reset the current bulk - bulks.push(currentBulk); - currentBulk = []; - currentBulkSize = 0; + bulks.push(currentBulk) + currentBulk = [] + currentBulkSize = 0 } else { // otherwise, add document to current bulk - currentBulk.push(data[documentIndex]); - currentBulkSize += currentDocumentSize; - documentIndex++; + currentBulk.push(data[documentIndex]) + currentBulkSize += currentDocumentSize + documentIndex++ } } - return bulks; + return bulks } /** @@ -339,57 +367,57 @@ function getBulksFromDocuments(data) { * @param {Object} indexName the index name * @param {Object} logger the logger object */ -async function indexBulkDataToES(modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); +async function indexBulkDataToES (modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) logger.info({ component: 'indexBulkDataToES', - message: `Reindexing of ${modelName}s started!`, - }); + message: `Reindexing of ${modelName}s started!` + }) - const esClient = getESClient(); + const esClient = getESClient() // clear index - const indexExistsRes = await esClient.indices.exists({ index: indexName }); + const indexExistsRes = await esClient.indices.exists({ index: indexName }) if (indexExistsRes.statusCode !== 404) { - await deleteIndex(indexName, logger, esClient); + await deleteIndex(indexName, logger, esClient) } - await createIndex(indexName, logger, esClient); + await createIndex(indexName, logger, esClient) // get data from db logger.info({ component: 'indexBulkDataToES', - message: 'Getting data from database', - }); - const model = models[modelName]; - const data = await model.findAll({ include }); - const rawObjects = _.map(data, (r) => r.toJSON()); + message: 'Getting data from database' + }) + const model = models[modelName] + const data = await model.findAll({ include }) + const rawObjects = _.map(data, (r) => r.toJSON()) if (_.isEmpty(rawObjects)) { logger.info({ component: 'indexBulkDataToES', - message: `No data in database for ${modelName}`, - }); - return; + message: `No data in database for ${modelName}` + }) + return } - const bulks = getBulksFromDocuments(rawObjects); + const bulks = getBulksFromDocuments(rawObjects) - const startTime = Date.now(); - let doneCount = 0; + const startTime = Date.now() + let doneCount = 0 for (const bulk of bulks) { // send bulk to esclient const body = bulk.flatMap((doc) => [ { index: { _index: indexName, _id: doc.id } }, - doc, - ]); - await esClient.bulk({ refresh: true, body }); - doneCount += bulk.length; + doc + ]) + await esClient.bulk({ refresh: true, body }) + doneCount += bulk.length // log metrics - const timeSpent = Date.now() - startTime; - const avgTimePerDocument = timeSpent / doneCount; - const estimatedLength = avgTimePerDocument * data.length; - const timeLeft = startTime + estimatedLength - Date.now(); + const timeSpent = Date.now() - startTime + const avgTimePerDocument = timeSpent / doneCount + const estimatedLength = avgTimePerDocument * data.length + const timeLeft = startTime + estimatedLength - Date.now() logger.info({ component: 'indexBulkDataToES', message: `Processed ${doneCount} of ${ @@ -398,8 +426,8 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { avgTimePerDocument )}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime( timeLeft - )}`, - }); + )}` + }) } } @@ -410,36 +438,36 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { * @param {string} id the job id * @param {Object} logger the logger object */ -async function indexDataToEsById(id, modelOpts, indexName, logger) { - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); +async function indexDataToEsById (id, modelOpts, indexName, logger) { + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) logger.info({ component: 'indexDataToEsById', - message: `Reindexing of ${modelName} with id ${id} started!`, - }); - const esClient = getESClient(); + message: `Reindexing of ${modelName} with id ${id} started!` + }) + const esClient = getESClient() logger.info({ component: 'indexDataToEsById', - message: 'Getting data from database', - }); - const model = models[modelName]; + message: 'Getting data from database' + }) + const model = models[modelName] - const data = await model.findById(id, include); + const data = await model.findById(id, include) logger.info({ component: 'indexDataToEsById', - message: 'Indexing data into Elasticsearch', - }); + message: 'Indexing data into Elasticsearch' + }) await esClient.index({ index: indexName, id: id, - body: data.dataValues, - }); + body: data.dataValues + }) logger.info({ component: 'indexDataToEsById', - message: 'Indexing complete!', - }); + message: 'Indexing complete!' + }) } /** @@ -448,68 +476,68 @@ async function indexDataToEsById(id, modelOpts, indexName, logger) { * @param {Array} dataModels the data models to import * @param {Object} logger the logger object */ -async function importData(pathToFile, dataModels, logger) { +async function importData (pathToFile, dataModels, logger) { // check if file exists if (!fs.existsSync(pathToFile)) { - throw new Error(`File with path ${pathToFile} does not exist`); + throw new Error(`File with path ${pathToFile} does not exist`) } // clear database - logger.info({ component: 'importData', message: 'Clearing database...' }); - await models.sequelize.sync({ force: true }); + logger.info({ component: 'importData', message: 'Clearing database...' }) + await models.sequelize.sync({ force: true }) - let transaction = null; - let currentModelName = null; + let transaction = null + let currentModelName = null try { // Start a transaction - transaction = await models.sequelize.transaction(); - const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()); + transaction = await models.sequelize.transaction() + const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()) for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index]; - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); + const modelOpts = dataModels[index] + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) - currentModelName = modelName; - const model = models[modelName]; - const modelRecords = jsonData[modelName]; + currentModelName = modelName + const model = models[modelName] + const modelRecords = jsonData[modelName] if (modelRecords && modelRecords.length > 0) { logger.info({ component: 'importData', - message: `Importing data for model: ${modelName}`, - }); + message: `Importing data for model: ${modelName}` + }) - await model.bulkCreate(modelRecords, { include, transaction }); + await model.bulkCreate(modelRecords, { include, transaction }) logger.info({ component: 'importData', - message: `Records imported for model: ${modelName} = ${modelRecords.length}`, - }); + message: `Records imported for model: ${modelName} = ${modelRecords.length}` + }) } else { logger.info({ component: 'importData', - message: `No records to import for model: ${modelName}`, - }); + message: `No records to import for model: ${modelName}` + }) } } // commit transaction only if all things went ok logger.info({ component: 'importData', - message: 'committing transaction to database...', - }); - await transaction.commit(); + message: 'committing transaction to database...' + }) + await transaction.commit() } catch (error) { logger.error({ component: 'importData', - message: `Error while writing data of model: ${currentModelName}`, - }); + message: `Error while writing data of model: ${currentModelName}` + }) // rollback all insert operations if (transaction) { logger.info({ component: 'importData', - message: 'rollback database transaction...', - }); - transaction.rollback(); + message: 'rollback database transaction...' + }) + transaction.rollback() } if (error.name && error.errors && error.fields) { // For sequelize validation errors, we throw only fields with data that helps in debugging error, @@ -519,11 +547,11 @@ async function importData(pathToFile, dataModels, logger) { modelName: currentModelName, name: error.name, errors: error.errors, - fields: error.fields, + fields: error.fields }) - ); + ) } else { - throw error; + throw error } } @@ -533,10 +561,10 @@ async function importData(pathToFile, dataModels, logger) { include: [ { model: models.Interview, - as: 'interviews', - }, - ], - }; + as: 'interviews' + } + ] + } const resourceBookingModelOpts = { modelName: 'ResourceBooking', include: [ @@ -546,23 +574,24 @@ async function importData(pathToFile, dataModels, logger) { include: [ { model: models.WorkPeriodPayment, - as: 'payments', - }, - ], - }, - ], - }; - await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger); + as: 'payments' + } + ] + } + ] + } + await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) await indexBulkDataToES( jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger - ); + ) await indexBulkDataToES( resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger - ); + ) + await indexBulkDataToES('Role', config.get('esConfig.ES_INDEX_ROLE'), logger) } /** @@ -571,74 +600,74 @@ async function importData(pathToFile, dataModels, logger) { * @param {Array} dataModels the data models to export * @param {Object} logger the logger object */ -async function exportData(pathToFile, dataModels, logger) { +async function exportData (pathToFile, dataModels, logger) { logger.info({ component: 'exportData', - message: `Start Saving data to file with path ${pathToFile}....`, - }); + message: `Start Saving data to file with path ${pathToFile}....` + }) - const allModelsRecords = {}; + const allModelsRecords = {} for (let index = 0; index < dataModels.length; index += 1) { - const modelOpts = dataModels[index]; - const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; - const include = _.get(modelOpts, 'include', []); - const modelRecords = await models[modelName].findAll({ include }); - const rawRecords = _.map(modelRecords, (r) => r.toJSON()); - allModelsRecords[modelName] = rawRecords; + const modelOpts = dataModels[index] + const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName + const include = _.get(modelOpts, 'include', []) + const modelRecords = await models[modelName].findAll({ include }) + const rawRecords = _.map(modelRecords, (r) => r.toJSON()) + allModelsRecords[modelName] = rawRecords logger.info({ component: 'exportData', - message: `Records loaded for model: ${modelName} = ${rawRecords.length}`, - }); + message: `Records loaded for model: ${modelName} = ${rawRecords.length}` + }) } - fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)); + fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)) logger.info({ component: 'exportData', - message: 'End Saving data to file....', - }); + message: 'End Saving data to file....' + }) } /** * Format a time in milliseconds into a human readable format * @param {Date} milliseconds the number of milliseconds */ -function formatTime(millisec) { - const ms = Math.floor(millisec % 1000); - const secs = Math.floor((millisec / 1000) % 60); - const mins = Math.floor((millisec / (1000 * 60)) % 60); - const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24); - const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7); - const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4); - const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12); - const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)); - - let formattedTime = '0 milliseconds'; +function formatTime (millisec) { + const ms = Math.floor(millisec % 1000) + const secs = Math.floor((millisec / 1000) % 60) + const mins = Math.floor((millisec / (1000 * 60)) % 60) + const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24) + const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7) + const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4) + const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12) + const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)) + + let formattedTime = '0 milliseconds' if (ms > 0) { - formattedTime = `${ms} milliseconds`; + formattedTime = `${ms} milliseconds` } if (secs > 0) { - formattedTime = `${secs} seconds ${formattedTime}`; + formattedTime = `${secs} seconds ${formattedTime}` } if (mins > 0) { - formattedTime = `${mins} minutes ${formattedTime}`; + formattedTime = `${mins} minutes ${formattedTime}` } if (hrs > 0) { - formattedTime = `${hrs} hours ${formattedTime}`; + formattedTime = `${hrs} hours ${formattedTime}` } if (days > 0) { - formattedTime = `${days} days ${formattedTime}`; + formattedTime = `${days} days ${formattedTime}` } if (weeks > 0) { - formattedTime = `${weeks} weeks ${formattedTime}`; + formattedTime = `${weeks} weeks ${formattedTime}` } if (mnths > 0) { - formattedTime = `${mnths} months ${formattedTime}`; + formattedTime = `${mnths} months ${formattedTime}` } if (yrs > 0) { - formattedTime = `${yrs} years ${formattedTime}`; + formattedTime = `${yrs} years ${formattedTime}` } - return formattedTime.trim(); + return formattedTime.trim() } /** @@ -647,30 +676,30 @@ function formatTime(millisec) { * @param {Array} source the array in which to search for the term * @param {Array | String} term the term to search */ -function checkIfExists(source, term) { - let terms; +function checkIfExists (source, term) { + let terms if (!_.isArray(source)) { - throw new Error('Source argument should be an array'); + throw new Error('Source argument should be an array') } - source = source.map((s) => s.toLowerCase()); + source = source.map((s) => s.toLowerCase()) if (_.isString(term)) { - terms = term.toLowerCase().split(' '); + terms = term.toLowerCase().split(' ') } else if (_.isArray(term)) { - terms = term.map((t) => t.toLowerCase()); + terms = term.map((t) => t.toLowerCase()) } else { - throw new Error('Term argument should be either a string or an array'); + throw new Error('Term argument should be either a string or an array') } for (let i = 0; i < terms.length; i++) { if (source.includes(terms[i])) { - return true; + return true } } - return false; + return false } /** @@ -678,10 +707,10 @@ function checkIfExists(source, term) { * @param {Function} fn the async function * @returns {Function} the wrapped function */ -function wrapExpress(fn) { +function wrapExpress (fn) { return function (req, res, next) { - fn(req, res, next).catch(next); - }; + fn(req, res, next).catch(next) + } } /** @@ -689,20 +718,20 @@ function wrapExpress(fn) { * @param obj the object (controller exports) * @returns {Object|Array} the wrapped object */ -function autoWrapExpress(obj) { +function autoWrapExpress (obj) { if (_.isArray(obj)) { - return obj.map(autoWrapExpress); + return obj.map(autoWrapExpress) } if (_.isFunction(obj)) { if (obj.constructor.name === 'AsyncFunction') { - return wrapExpress(obj); + return wrapExpress(obj) } - return obj; + return obj } _.each(obj, (value, key) => { - obj[key] = autoWrapExpress(value); - }); - return obj; + obj[key] = autoWrapExpress(value) + }) + return obj } /** @@ -711,11 +740,11 @@ function autoWrapExpress(obj) { * @param {Number} page the page number * @returns {String} link for the page */ -function getPageLink(req, page) { - const q = _.assignIn({}, req.query, { page }); +function getPageLink (req, page) { + const q = _.assignIn({}, req.query, { page }) return `${req.protocol}://${req.get('Host')}${req.baseUrl}${ req.path - }?${querystring.stringify(q)}`; + }?${querystring.stringify(q)}` } /** @@ -724,31 +753,31 @@ function getPageLink(req, page) { * @param {Object} res the HTTP response * @param {Object} result the operation result */ -function setResHeaders(req, res, result) { - const totalPages = Math.ceil(result.total / result.perPage); +function setResHeaders (req, res, result) { + const totalPages = Math.ceil(result.total / result.perPage) if (result.page > 1) { - res.set('X-Prev-Page', result.page - 1); + res.set('X-Prev-Page', result.page - 1) } if (result.page < totalPages) { - res.set('X-Next-Page', result.page + 1); + res.set('X-Next-Page', result.page + 1) } - res.set('X-Page', result.page); - res.set('X-Per-Page', result.perPage); - res.set('X-Total', result.total); - res.set('X-Total-Pages', totalPages); + res.set('X-Page', result.page) + res.set('X-Per-Page', result.perPage) + res.set('X-Total', result.total) + res.set('X-Total-Pages', totalPages) // set Link header if (totalPages > 0) { let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink( req, totalPages - )}>; rel="last"`; + )}>; rel="last"` if (result.page > 1) { - link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"`; + link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"` } if (result.page < totalPages) { - link += `, <${getPageLink(req, result.page + 1)}>; rel="next"`; + link += `, <${getPageLink(req, result.page + 1)}>; rel="next"` } - res.set('Link', link); + res.set('Link', link) } } @@ -756,30 +785,30 @@ function setResHeaders(req, res, result) { * Get ES Client * @return {Object} Elastic Host Client Instance */ -function getESClient() { +function getESClient () { if (esClients.client) { - return esClients.client; + return esClients.client } - const host = config.esConfig.HOST; - const cloudId = config.esConfig.ELASTICCLOUD.id; + const host = config.esConfig.HOST + const cloudId = config.esConfig.ELASTICCLOUD.id if (cloudId) { // Elastic Cloud configuration esClients.client = new elasticsearch.Client({ cloud: { - id: cloudId, + id: cloudId }, auth: { username: config.esConfig.ELASTICCLOUD.username, - password: config.esConfig.ELASTICCLOUD.password, - }, - }); + password: config.esConfig.ELASTICCLOUD.password + } + }) } else { esClients.client = new elasticsearch.Client({ - node: host, - }); + node: host + }) } - return esClients.client; + return esClients.client } /* @@ -790,8 +819,8 @@ const getM2MToken = async () => { return await m2m.getMachineToken( config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET - ); -}; + ) +} /* * Function to get M2M token for U-Bahn @@ -801,8 +830,8 @@ const getM2MUbahnToken = async () => { return await m2mForUbahn.getMachineToken( config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET - ); -}; + ) +} /** * Function to encode query string @@ -810,17 +839,17 @@ const getM2MUbahnToken = async () => { * @param {String} nesting the nesting string * @returns {String} query string */ -function encodeQueryString(queryObj, nesting = '') { +function encodeQueryString (queryObj, nesting = '') { const pairs = Object.entries(queryObj).map(([key, val]) => { // Handle the nested, recursive case, where the value to encode is an object itself if (typeof val === 'object') { - return encodeQueryString(val, nesting + `${key}.`); + return encodeQueryString(val, nesting + `${key}.`) } else { // Handle base case, where the value to encode is simply a string. - return [nesting + key, val].map(querystring.escape).join('='); + return [nesting + key, val].map(querystring.escape).join('=') } - }); - return pairs.join('&'); + }) + return pairs.join('&') } /** @@ -828,31 +857,31 @@ function encodeQueryString(queryObj, nesting = '') { * @param {Integer} externalId the legacy user id * @returns {Array} the users found */ -async function listUsersByExternalId(externalId) { +async function listUsersByExternalId (externalId) { // return empty list if externalId is null or undefined if (!!externalId !== true) { - return []; + return [] } - const token = await getM2MUbahnToken(); + const token = await getM2MUbahnToken() const q = { enrich: true, externalProfile: { organizationId: config.ORG_ID, - externalId, - }, - }; - const url = `${config.TC_API}/users?${encodeQueryString(q)}`; + externalId + } + } + const url = `${config.TC_API}/users?${encodeQueryString(q)}` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listUserByExternalId', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -860,14 +889,14 @@ async function listUsersByExternalId(externalId) { * @param {Integer} externalId the legacy user id * @returns {Object} the user */ -async function getUserByExternalId(externalId) { - const users = await listUsersByExternalId(externalId); +async function getUserByExternalId (externalId) { + const users = await listUsersByExternalId(externalId) if (_.isEmpty(users)) { throw new errors.NotFoundError( `externalId: ${externalId} "user" not found` - ); + ) } - return users[0]; + return users[0] } /** @@ -876,24 +905,24 @@ async function getUserByExternalId(externalId) { * @params {Object} payload the payload * @params {Object} options the extra options to control the function */ -async function postEvent(topic, payload, options = {}) { +async function postEvent (topic, payload, options = {}) { logger.debug({ component: 'helper', context: 'postEvent', message: `Posting event to Kafka topic ${topic}, ${JSON.stringify( payload - )}`, - }); - const client = getBusApiClient(); + )}` + }) + const client = getBusApiClient() const message = { topic, originator: config.KAFKA_MESSAGE_ORIGINATOR, timestamp: new Date().toISOString(), 'mime-type': 'application/json', - payload, - }; - await client.postEvent(message); - await eventDispatcher.handleEvent(topic, { value: payload, options }); + payload + } + await client.postEvent(message) + await eventDispatcher.handleEvent(topic, { value: payload, options }) } /** @@ -902,11 +931,11 @@ async function postEvent(topic, payload, options = {}) { * @param {Object} err the err * @returns {Boolean} the result */ -function isDocumentMissingException(err) { +function isDocumentMissingException (err) { if (err.statusCode === 404 && err instanceof ESResponseError) { - return true; + return true } - return false; + return false } /** @@ -915,34 +944,34 @@ function isDocumentMissingException(err) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getProjects(currentUser, criteria = {}) { - let token; +async function getProjects (currentUser, criteria = {}) { + let token if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken(); - token = `Bearer ${m2mToken}`; + const m2mToken = await getM2MToken() + token = `Bearer ${m2mToken}` } else { - token = currentUser.jwtToken; + token = currentUser.jwtToken } - const url = `${config.TC_API}/projects?type=talent-as-a-service`; + const url = `${config.TC_API}/projects?type=talent-as-a-service` const res = await request .get(url) .query(criteria) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getProjects', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) const result = _.map(res.body, (item) => { - return _.pick(item, ['id', 'name', 'invites', 'members']); - }); + return _.pick(item, ['id', 'name', 'invites', 'members']) + }) return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result, - }; + result + } } /** @@ -951,24 +980,24 @@ async function getProjects(currentUser, criteria = {}) { * @param {String} userId the legacy user id * @returns {Object} the user */ -async function getTopcoderUserById(userId) { - const token = await getM2MToken(); +async function getTopcoderUserById (userId) { + const token = await getM2MToken() const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `id=${userId}` }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getTopcoderUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - const user = _.get(res.body, 'result.content[0]'); + message: `response body: ${JSON.stringify(res.body)}` + }) + const user = _.get(res.body, 'result.content[0]') if (!user) { throw new errors.NotFoundError( `userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}` - ); + ) } - return user; + return user } /** @@ -976,31 +1005,31 @@ async function getTopcoderUserById(userId) { * @param {String} userId the user id * @returns the request result */ -async function getUserById(userId, enrich) { - const token = await getM2MUbahnToken(); +async function getUserById (userId, enrich) { + const token = await getM2MUbahnToken() const res = await request .get(`${config.TC_API}/users/${userId}` + (enrich ? '?enrich=true' : '')) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) - const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']); + const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']) if (enrich) { user.skills = (res.body.skills || []).map((skillObj) => _.pick(skillObj.skill, ['id', 'name']) - ); - const attributes = _.get(res, 'body.attributes', []); + ) + const attributes = _.get(res, 'body.attributes', []) user.attributes = _.map(attributes, (attr) => _.pick(attr, ['id', 'value', 'attribute.id', 'attribute.name']) - ); + ) } - return user; + return user } /** @@ -1008,19 +1037,19 @@ async function getUserById(userId, enrich) { * @param {Object} data the user data * @returns the request result */ -async function createUbahnUser({ handle, firstName, lastName }) { - const token = await getM2MUbahnToken(); +async function createUbahnUser ({ handle, firstName, lastName }) { + const token = await getM2MUbahnToken() const res = await request .post(`${config.TC_API}/users`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ handle, firstName, lastName }); + .send({ handle, firstName, lastName }) localLogger.debug({ context: 'createUbahnUser', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id']) } /** @@ -1028,21 +1057,21 @@ async function createUbahnUser({ handle, firstName, lastName }) { * @param {String} userId the user id(with uuid format) * @param {Object} data the profile data */ -async function createUserExternalProfile( +async function createUserExternalProfile ( userId, { organizationId, externalId } ) { - const token = await getM2MUbahnToken(); + const token = await getM2MUbahnToken() const res = await request .post(`${config.TC_API}/users/${userId}/externalProfiles`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send({ organizationId, externalId: String(externalId) }); + .send({ organizationId, externalId: String(externalId) }) localLogger.debug({ context: 'createUserExternalProfile', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) } /** @@ -1050,23 +1079,23 @@ async function createUserExternalProfile( * @param {Array} handles the handle array * @returns the request result */ -async function getMembers(handles) { - const token = await getM2MToken(); +async function getMembers (handles) { + const token = await getM2MToken() const handlesStr = _.map(handles, (handle) => { - return '%22' + handle.toLowerCase() + '%22'; - }).join(','); - const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]`; + return '%22' + handle.toLowerCase() + '%22' + }).join(',') + const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getMembers', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } /** @@ -1075,36 +1104,36 @@ async function getMembers(handles) { * @param {Number} id project id * @returns the request result */ -async function getProjectById(currentUser, id) { - let token; +async function getProjectById (currentUser, id) { + let token if (currentUser.hasManagePermission || currentUser.isMachine) { - const m2mToken = await getM2MToken(); - token = `Bearer ${m2mToken}`; + const m2mToken = await getM2MToken() + token = `Bearer ${m2mToken}` } else { - token = currentUser.jwtToken; + token = currentUser.jwtToken } - const url = `${config.TC_API}/projects/${id}`; + const url = `${config.TC_API}/projects/${id}` try { const res = await request .get(url) .set('Authorization', token) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getProjectById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id', 'name', 'invites', 'members']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id', 'name', 'invites', 'members']) } catch (err) { if (err.status === HttpStatus.FORBIDDEN) { throw new errors.ForbiddenError( `You are not allowed to access the project with id ${id}` - ); + ) } if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${id} project not found`); + throw new errors.NotFoundError(`id: ${id} project not found`) } - throw err; + throw err } } @@ -1115,33 +1144,33 @@ async function getProjectById(currentUser, id) { * @param {Object} criteria the search criteria * @returns the request result */ -async function getTopcoderSkills(criteria) { - const token = await getM2MUbahnToken(); +async function getTopcoderSkills (criteria) { + const token = await getM2MUbahnToken() try { const res = await request .get(`${config.TC_API}/skills`) .query({ skillProviderId: config.TOPCODER_SKILL_PROVIDER_ID, - ...criteria, + ...criteria }) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getTopcoderSkills', - message: `response body: ${JSON.stringify(res.body)}`, - }); + message: `response body: ${JSON.stringify(res.body)}` + }) return { total: Number(_.get(res.headers, 'x-total')), page: Number(_.get(res.headers, 'x-page')), perPage: Number(_.get(res.headers, 'x-per-page')), - result: res.body, - }; + result: res.body + } } catch (err) { if (err.status === HttpStatus.BAD_REQUEST) { - throw new errors.BadRequestError(err.response.body.message); + throw new errors.BadRequestError(err.response.body.message) } - throw err; + throw err } } @@ -1150,18 +1179,18 @@ async function getTopcoderSkills(criteria) { * @param {String} skillId the skill Id * @returns the request result */ -async function getSkillById(skillId) { - const token = await getM2MUbahnToken(); +async function getSkillById (skillId) { + const token = await getM2MUbahnToken() const res = await request .get(`${config.TC_API}/skills/${skillId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getSkillById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.pick(res.body, ['id', 'name']); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.pick(res.body, ['id', 'name']) } /** @@ -1174,22 +1203,22 @@ async function getSkillById(skillId) { * @params {Object} currentUser the user who perform this operation * @returns {String} the ubahn user id */ -async function ensureUbahnUserId(currentUser) { +async function ensureUbahnUserId (currentUser) { try { - return (await getUserByExternalId(currentUser.userId)).id; + return (await getUserByExternalId(currentUser.userId)).id } catch (err) { if (!(err instanceof errors.NotFoundError)) { - throw err; + throw err } - const topcoderUser = await getTopcoderUserById(currentUser.userId); + const topcoderUser = await getTopcoderUserById(currentUser.userId) const user = await createUbahnUser( _.pick(topcoderUser, ['handle', 'firstName', 'lastName']) - ); + ) await createUserExternalProfile(user.id, { organizationId: config.ORG_ID, - externalId: currentUser.userId, - }); - return user.id; + externalId: currentUser.userId + }) + return user.id } } @@ -1199,8 +1228,8 @@ async function ensureUbahnUserId(currentUser) { * @param {String} jobId the job id * @returns {Object} the job data */ -async function ensureJobById(jobId) { - return models.Job.findById(jobId); +async function ensureJobById (jobId) { + return models.Job.findById(jobId) } /** @@ -1209,8 +1238,8 @@ async function ensureJobById(jobId) { * @param {String} resourceBookingId the resourceBooking id * @returns {Object} the resourceBooking data */ -async function ensureResourceBookingById(resourceBookingId) { - return models.ResourceBooking.findById(resourceBookingId); +async function ensureResourceBookingById (resourceBookingId) { + return models.ResourceBooking.findById(resourceBookingId) } /** @@ -1218,8 +1247,8 @@ async function ensureResourceBookingById(resourceBookingId) { * @param {String} workPeriodId the workPeriod id * @returns the workPeriod data */ -async function ensureWorkPeriodById(workPeriodId) { - return models.WorkPeriod.findById(workPeriodId); +async function ensureWorkPeriodById (workPeriodId) { + return models.WorkPeriod.findById(workPeriodId) } /** @@ -1228,24 +1257,24 @@ async function ensureWorkPeriodById(workPeriodId) { * @param {String} jobId the user id * @returns {Object} the user data */ -async function ensureUserById(userId) { - const token = await getM2MUbahnToken(); +async function ensureUserById (userId) { + const token = await getM2MUbahnToken() try { const res = await request .get(`${config.TC_API}/users/${userId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'ensureUserById', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return res.body; + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError(`id: ${userId} "user" not found`); + throw new errors.NotFoundError(`id: ${userId} "user" not found`) } - throw err; + throw err } } @@ -1254,12 +1283,12 @@ async function ensureUserById(userId) { * * @returns {Object} the M2M auth user */ -function getAuditM2Muser() { +function getAuditM2Muser () { return { isMachine: true, userId: config.m2m.M2M_AUDIT_USER_ID, - handle: config.m2m.M2M_AUDIT_HANDLE, - }; + handle: config.m2m.M2M_AUDIT_HANDLE + } } /** @@ -1271,24 +1300,24 @@ function getAuditM2Muser() { * @param {Number} projectId project id * @returns the result */ -async function checkIsMemberOfProject(userId, projectId) { - const m2mToken = await getM2MToken(); +async function checkIsMemberOfProject (userId, projectId) { + const m2mToken = await getM2MToken() const res = await request .get(`${config.TC_API}/projects/${projectId}`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const memberIdList = _.map(res.body.members, 'userId'); + .set('Accept', 'application/json') + const memberIdList = _.map(res.body.members, 'userId') localLogger.debug({ context: 'checkIsMemberOfProject', message: `the members of project ${projectId}: ${JSON.stringify( memberIdList - )}, authUserId: ${JSON.stringify(userId)}`, - }); + )}, authUserId: ${JSON.stringify(userId)}` + }) if (!memberIdList.includes(userId)) { throw new errors.UnauthorizedError( `userId: ${userId} the user is not a member of project ${projectId}` - ); + ) } } @@ -1298,11 +1327,11 @@ async function checkIsMemberOfProject(userId, projectId) { * @param {Array} handles the array of handles * @returns {Array} the member details */ -async function getMemberDetailsByHandles(handles) { +async function getMemberDetailsByHandles (handles) { if (!handles.length) { - return []; + return [] } - const token = await getM2MToken(); + const token = await getM2MToken() const res = await request .get(`${config.TOPCODER_MEMBERS_API}/_search`) .query({ @@ -1310,15 +1339,15 @@ async function getMemberDetailsByHandles(handles) { handles, (handle) => `handleLower:${handle.toLowerCase()}` ).join(' OR '), - fields: 'userId,handle,firstName,lastName,email', + fields: 'userId,handle,firstName,lastName,email' }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getMemberDetailsByHandles', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res.body, 'result.content'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res.body, 'result.content') } /** @@ -1327,17 +1356,17 @@ async function getMemberDetailsByHandles(handles) { * @param {String} handle the user handle * @returns {Object} the member details */ -async function getV3MemberDetailsByHandle(handle) { - const token = await getM2MToken(); +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'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getV3MemberDetailsByHandle', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res.body, 'result.content'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res.body, 'result.content') } /** @@ -1347,20 +1376,20 @@ async function getV3MemberDetailsByHandle(handle) { * @param {String} email the email * @returns {Array} the member details */ -async function _getMemberDetailsByEmail(token, email) { +async function _getMemberDetailsByEmail (token, email) { const res = await request .get(config.TOPCODER_USERS_API) .query({ filter: `email=${email}`, - fields: 'handle,id,email,firstName,lastName', + fields: 'handle,id,email,firstName,lastName' }) .set('Authorization', `Bearer ${token}`) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: '_getMemberDetailsByEmail', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res.body, 'result.content'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res.body, 'result.content') } /** @@ -1370,25 +1399,25 @@ async function _getMemberDetailsByEmail(token, email) { * @param {Array} emails the array of emails * @returns {Array} the member details */ -async function getMemberDetailsByEmails(emails) { - const token = await getM2MToken(); +async function getMemberDetailsByEmails (emails) { + const token = await getM2MToken() const limiter = new Bottleneck({ - maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API, - }); + maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API + }) const membersArray = await Promise.all( emails.map((email) => limiter.schedule(() => _getMemberDetailsByEmail(token, email).catch((error) => { localLogger.error({ context: 'getMemberDetailsByEmails', - message: error.message, - }); - return []; + message: error.message + }) + return [] }) ) ) - ); - return _.flatten(membersArray); + ) + return _.flatten(membersArray) } /** @@ -1399,20 +1428,20 @@ async function getMemberDetailsByEmails(emails) { * @param {Object} criteria the filtering criteria * @returns {Object} the member created */ -async function createProjectMember(projectId, data, criteria) { - const m2mToken = await getM2MToken(); +async function createProjectMember (projectId, data, criteria) { + const m2mToken = await getM2MToken() const { body: member } = await request .post(`${config.TC_API}/projects/${projectId}/members`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') .query(criteria) - .send(data); + .send(data) localLogger.debug({ context: 'createProjectMember', - message: `response body: ${JSON.stringify(member)}`, - }); - return member; + message: `response body: ${JSON.stringify(member)}` + }) + return member } /** @@ -1422,21 +1451,21 @@ async function createProjectMember(projectId, data, criteria) { * @param {Object} criteria the search criteria * @returns {Array} the project members */ -async function listProjectMembers(currentUser, projectId, criteria = {}) { +async function listProjectMembers (currentUser, projectId, criteria = {}) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken const { body: members } = await request .get(`${config.TC_API}/projects/${projectId}/members`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listProjectMembers', - message: `response body: ${JSON.stringify(members)}`, - }); - return members; + message: `response body: ${JSON.stringify(members)}` + }) + return members } /** @@ -1446,21 +1475,21 @@ async function listProjectMembers(currentUser, projectId, criteria = {}) { * @param {Object} criteria the search criteria * @returns {Array} the member invites */ -async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { +async function listProjectMemberInvites (currentUser, projectId, criteria = {}) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken const { body: invites } = await request .get(`${config.TC_API}/projects/${projectId}/invites`) .query(criteria) .set('Authorization', token) - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'listProjectMemberInvites', - message: `response body: ${JSON.stringify(invites)}`, - }); - return invites; + message: `response body: ${JSON.stringify(invites)}` + }) + return invites } /** @@ -1470,24 +1499,24 @@ async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { * @param {String} projectMemberId the id of the project member * @returns {undefined} */ -async function deleteProjectMember(currentUser, projectId, projectMemberId) { +async function deleteProjectMember (currentUser, projectId, projectMemberId) { const token = currentUser.hasManagePermission || currentUser.isMachine ? `Bearer ${await getM2MToken()}` - : currentUser.jwtToken; + : currentUser.jwtToken try { await request .delete( `${config.TC_API}/projects/${projectId}/members/${projectMemberId}` ) - .set('Authorization', token); + .set('Authorization', token) } catch (err) { if (err.status === HttpStatus.NOT_FOUND) { throw new errors.NotFoundError( `projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}` - ); + ) } - throw err; + throw err } } @@ -1497,13 +1526,13 @@ async function deleteProjectMember(currentUser, projectId, projectMemberId) { * @param {String} attributeName Requested attribute name, e.g. "email" * @returns attribute value */ -function getUserAttributeValue(user, attributeName) { - const attributes = _.get(user, 'attributes', []); +function getUserAttributeValue (user, attributeName) { + const attributes = _.get(user, 'attributes', []) const targetAttribute = _.find( attributes, (a) => a.attribute.name === attributeName - ); - return _.get(targetAttribute, 'value'); + ) + return _.get(targetAttribute, 'value') } /** @@ -1513,34 +1542,34 @@ function getUserAttributeValue(user, attributeName) { * @param {String} token m2m token * @returns {Object} the challenge created */ -async function createChallenge(data, token) { +async function createChallenge (data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/challenges`; + const url = `${config.TC_API}/challenges` localLogger.debug({ context: 'createChallenge', - message: `EndPoint: POST ${url}`, - }); + message: `EndPoint: POST ${url}` + }) localLogger.debug({ context: 'createChallenge', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: challenge, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createChallenge', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'createChallenge', - message: `Response Body: ${JSON.stringify(challenge)}`, - }); - return challenge; + message: `Response Body: ${JSON.stringify(challenge)}` + }) + return challenge } /** @@ -1551,34 +1580,34 @@ async function createChallenge(data, token) { * @param {String} token m2m token * @returns {Object} the challenge updated */ -async function updateChallenge(challengeId, data, token) { +async function updateChallenge (challengeId, data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/challenges/${challengeId}`; + const url = `${config.TC_API}/challenges/${challengeId}` localLogger.debug({ context: 'updateChallenge', - message: `EndPoint: PATCH ${url}`, - }); + message: `EndPoint: PATCH ${url}` + }) localLogger.debug({ context: 'updateChallenge', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: challenge, status: httpStatus } = await request .patch(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'updateChallenge', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'updateChallenge', - message: `Response Body: ${JSON.stringify(challenge)}`, - }); - return challenge; + message: `Response Body: ${JSON.stringify(challenge)}` + }) + return challenge } /** @@ -1588,34 +1617,34 @@ async function updateChallenge(challengeId, data, token) { * @param {String} token m2m token * @returns {Object} the resource created */ -async function createChallengeResource(data, token) { +async function createChallengeResource (data, token) { if (!token) { - token = await getM2MToken(); + token = await getM2MToken() } - const url = `${config.TC_API}/resources`; + const url = `${config.TC_API}/resources` localLogger.debug({ context: 'createChallengeResource', - message: `EndPoint: POST ${url}`, - }); + message: `EndPoint: POST ${url}` + }) localLogger.debug({ context: 'createChallengeResource', - message: `Request Body: ${JSON.stringify(data)}`, - }); + message: `Request Body: ${JSON.stringify(data)}` + }) const { body: resource, status: httpStatus } = await request .post(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createChallengeResource', - message: `Status Code: ${httpStatus}`, - }); + message: `Status Code: ${httpStatus}` + }) localLogger.debug({ context: 'createChallengeResource', - message: `Response Body: ${JSON.stringify(resource)}`, - }); - return resource; + message: `Response Body: ${JSON.stringify(resource)}` + }) + return resource } /** @@ -1624,40 +1653,40 @@ async function createChallengeResource(data, token) { * @param {Date} end end date of the resource booking * @returns {Array<{startDate:Date, endDate:Date, daysWorked:number}>} information about workPeriods */ -function extractWorkPeriods(start, end) { +function extractWorkPeriods (start, end) { // calculate maximum possible daysWorked for a week - function getDaysWorked(week) { + function getDaysWorked (week) { if (weeks === 1) { - return Math.min(endDay, 5) - Math.max(startDay, 1) + 1; + return Math.min(endDay, 5) - Math.max(startDay, 1) + 1 } else if (week === 0) { - return Math.min(6 - startDay, 5); + return Math.min(6 - startDay, 5) } else if (week === weeks - 1) { - return Math.min(endDay, 5); - } else return 5; + return Math.min(endDay, 5) + } else return 5 } - const periods = []; + const periods = [] if (_.isNil(start) || _.isNil(end)) { - return periods; + return periods } - const startDate = moment(start); - const startDay = startDate.get('day'); - startDate.set('day', 0).startOf('day'); + const startDate = moment(start) + const startDay = startDate.get('day') + startDate.set('day', 0).startOf('day') - const endDate = moment(end); - const endDay = endDate.get('day'); - endDate.set('day', 6).endOf('day'); + const endDate = moment(end) + const endDay = endDate.get('day') + endDate.set('day', 6).endOf('day') - const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7; + const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7 for (let i = 0; i < weeks; i++) { periods.push({ startDate: startDate.format('YYYY-MM-DD'), endDate: startDate.add(6, 'day').format('YYYY-MM-DD'), - daysWorked: getDaysWorked(i), - }); - startDate.add(1, 'day'); + daysWorked: getDaysWorked(i) + }) + startDate.add(1, 'day') } - return periods; + return periods } /** @@ -1666,19 +1695,19 @@ function extractWorkPeriods(start, end) { * @param {String} userHandle user handle * @returns {String} email address of the user */ -async function getUserByHandle(userHandle) { - const token = await getM2MToken(); - const url = `${config.TC_API}/members/${userHandle}`; +async function getUserByHandle (userHandle) { + const token = await getM2MToken() + const url = `${config.TC_API}/members/${userHandle}` const res = await request .get(url) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + .set('Accept', 'application/json') localLogger.debug({ context: 'getUserByHandle', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res, 'body'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res, 'body') } /** @@ -1687,14 +1716,14 @@ async function getUserByHandle(userHandle) { * @param {*} object of json that would be replaced in string * @returns */ -async function substituteStringByObject(string, object) { +async function substituteStringByObject (string, object) { for (var key in object) { if (!Object.prototype.hasOwnProperty.call(object, key)) { - continue; + continue } - string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]); + string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]) } - return string; + return string } /** @@ -1702,19 +1731,19 @@ async function substituteStringByObject(string, object) { * @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 (currentUser, data) { + const token = currentUser.jwtToken const res = await request .post(`${config.TC_API}/projects/`) .set('Authorization', token) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .send(data); + .send(data) localLogger.debug({ context: 'createProject', - message: `response body: ${JSON.stringify(res)}`, - }); - return _.get(res, 'body'); + message: `response body: ${JSON.stringify(res)}` + }) + return _.get(res, 'body') } module.exports = { @@ -1733,9 +1762,9 @@ module.exports = { getUserId: async (userId) => { // check m2m user id if (userId === config.m2m.M2M_AUDIT_USER_ID) { - return config.m2m.M2M_AUDIT_USER_ID; + return config.m2m.M2M_AUDIT_USER_ID } - return ensureUbahnUserId({ userId }); + return ensureUbahnUserId({ userId }) }, getUserByExternalId, getM2MToken, @@ -1769,5 +1798,5 @@ module.exports = { extractWorkPeriods, getUserByHandle, substituteStringByObject, - createProject, -}; + createProject +} diff --git a/src/controllers/RoleController.js b/src/controllers/RoleController.js new file mode 100644 index 00000000..747cbe4d --- /dev/null +++ b/src/controllers/RoleController.js @@ -0,0 +1,59 @@ +/** + * Controller for Role endpoints + */ +const HttpStatus = require('http-status-codes') +const service = require('../services/RoleService') + +/** + * Get role by id + * @param req the request + * @param res the response + */ +async function getRole (req, res) { + res.send(await service.getRole(req.authUser, req.params.id, req.query.fromDb)) +} + +/** + * Create role + * @param req the request + * @param res the response + */ +async function createRole (req, res) { + res.send(await service.createRole(req.authUser, req.body)) +} + +/** + * update role by id + * @param req the request + * @param res the response + */ +async function updateRole (req, res) { + res.send(await service.updateRole(req.authUser, req.params.id, req.body)) +} + +/** + * Delete role by id + * @param req the request + * @param res the response + */ +async function deleteRole (req, res) { + await service.deleteRole(req.authUser, req.params.id) + res.status(HttpStatus.NO_CONTENT).end() +} + +/** + * Search roles + * @param req the request + * @param res the response + */ +async function searchRoles (req, res) { + res.send(await service.searchRoles(req.authUser, req.query)) +} + +module.exports = { + getRole, + createRole, + updateRole, + deleteRole, + searchRoles +} diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index ca4f1bca..26d70738 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -1,19 +1,19 @@ /** * Controller for TaaS teams endpoints */ -const HttpStatus = require('http-status-codes'); -const service = require('../services/TeamService'); -const helper = require('../common/helper'); +const HttpStatus = require('http-status-codes') +const service = require('../services/TeamService') +const helper = require('../common/helper') /** * Search teams * @param req the request * @param res the response */ -async function searchTeams(req, res) { - const result = await service.searchTeams(req.authUser, req.query); - helper.setResHeaders(req, res, result); - res.send(result.result); +async function searchTeams (req, res) { + const result = await service.searchTeams(req.authUser, req.query) + helper.setResHeaders(req, res, result) + res.send(result.result) } /** @@ -21,8 +21,8 @@ async function searchTeams(req, res) { * @param req the request * @param res the response */ -async function getTeam(req, res) { - res.send(await service.getTeam(req.authUser, req.params.id)); +async function getTeam (req, res) { + res.send(await service.getTeam(req.authUser, req.params.id)) } /** @@ -30,10 +30,10 @@ async function getTeam(req, res) { * @param req the request * @param res the response */ -async function getTeamJob(req, res) { +async function getTeamJob (req, res) { res.send( await service.getTeamJob(req.authUser, req.params.id, req.params.jobId) - ); + ) } /** @@ -41,9 +41,9 @@ async function getTeamJob(req, res) { * @param req the request * @param res the response */ -async function sendEmail(req, res) { - await service.sendEmail(req.authUser, req.body); - res.status(HttpStatus.NO_CONTENT).end(); +async function sendEmail (req, res) { + await service.sendEmail(req.authUser, req.body) + res.status(HttpStatus.NO_CONTENT).end() } /** @@ -51,10 +51,10 @@ async function sendEmail(req, res) { * @param req the request * @param res the response */ -async function addMembers(req, res) { +async function addMembers (req, res) { res.send( await service.addMembers(req.authUser, req.params.id, req.query, req.body) - ); + ) } /** @@ -62,13 +62,13 @@ async function addMembers(req, res) { * @param req the request * @param res the response */ -async function searchMembers(req, res) { +async function searchMembers (req, res) { const result = await service.searchMembers( req.authUser, req.params.id, req.query - ); - res.send(result.result); + ) + res.send(result.result) } /** @@ -76,13 +76,13 @@ async function searchMembers(req, res) { * @param req the request * @param res the response */ -async function searchInvites(req, res) { +async function searchInvites (req, res) { const result = await service.searchInvites( req.authUser, req.params.id, req.query - ); - res.send(result.result); + ) + res.send(result.result) } /** @@ -90,13 +90,13 @@ async function searchInvites(req, res) { * @param req the request * @param res the response */ -async function deleteMember(req, res) { +async function deleteMember (req, res) { await service.deleteMember( req.authUser, req.params.id, req.params.projectMemberId - ); - res.status(HttpStatus.NO_CONTENT).end(); + ) + res.status(HttpStatus.NO_CONTENT).end() } /** @@ -104,8 +104,8 @@ async function deleteMember(req, res) { * @param req the request * @param res the response */ -async function getMe(req, res) { - res.send(await service.getMe(req.authUser)); +async function getMe (req, res) { + res.send(await service.getMe(req.authUser)) } /** @@ -113,8 +113,8 @@ async function getMe(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 createProj (req, res) { + res.send(await service.createProj(req.authUser, req.body)) } module.exports = { @@ -127,5 +127,5 @@ module.exports = { searchInvites, deleteMember, getMe, - createProj, -}; + createProj +} diff --git a/src/eventHandlers/RoleEventHandler.js b/src/eventHandlers/RoleEventHandler.js new file mode 100644 index 00000000..38dbdb79 --- /dev/null +++ b/src/eventHandlers/RoleEventHandler.js @@ -0,0 +1,64 @@ +/* + * Handle events for ResourceBooking. + */ + +const { Op } = require('sequelize') +const _ = require('lodash') +const models = require('../models') +const logger = require('../common/logger') +const helper = require('../common/helper') +const JobService = require('../services/JobService') + +const Job = models.Job + +/** + * When a Role is deleted, jobs related to + * that role should be updated + * @param {object} payload the event payload + * @returns {undefined} + */ +async function updateJobs (payload) { + // find jobs have this role + const jobs = await Job.findAll({ + where: { + roleIds: { [Op.contains]: [payload.value.id] } + }, + raw: true + }) + if (jobs.length === 0) { + logger.debug({ + component: 'RoleEventHandler', + context: 'updateJobs', + message: `id: ${payload.value.id} role has no related job - ignored` + }) + return + } + const m2mUser = helper.getAuditM2Muser() + // remove role id from related jobs + await Promise.all(_.map(jobs, async job => { + let roleIds = _.filter(job.roleIds, roleId => roleId !== payload.value.id) + if (roleIds.length === 0) { + roleIds = null + } + await JobService.partiallyUpdateJob(m2mUser, job.id, { roleIds }) + })) + logger.debug({ + component: 'RoleEventHandler', + context: 'updateJobs', + message: `role id: ${payload.value.id} removed from jobs with id: ${_.map(jobs, 'id')}` + }) +} + +/** + * Process role delete event. + * + * @param {Object} payload the event payload + * @returns {undefined} + */ +async function processDelete (payload) { + await updateJobs(payload) +} + +module.exports = { + processDelete +} diff --git a/src/eventHandlers/index.js b/src/eventHandlers/index.js index 17445994..6e0ec2a8 100644 --- a/src/eventHandlers/index.js +++ b/src/eventHandlers/index.js @@ -8,6 +8,7 @@ const JobEventHandler = require('./JobEventHandler') const JobCandidateEventHandler = require('./JobCandidateEventHandler') const ResourceBookingEventHandler = require('./ResourceBookingEventHandler') const InterviewEventHandler = require('./InterviewEventHandler') +const RoleEventHandler = require('./RoleEventHandler') const logger = require('../common/logger') const TopicOperationMapping = { @@ -16,7 +17,8 @@ const TopicOperationMapping = { [config.TAAS_RESOURCE_BOOKING_CREATE_TOPIC]: ResourceBookingEventHandler.processCreate, [config.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC]: ResourceBookingEventHandler.processUpdate, [config.TAAS_RESOURCE_BOOKING_DELETE_TOPIC]: ResourceBookingEventHandler.processDelete, - [config.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewEventHandler.processRequest + [config.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewEventHandler.processRequest, + [config.TAAS_ROLE_DELETE_TOPIC]: RoleEventHandler.processDelete } /** diff --git a/src/models/Job.js b/src/models/Job.js index 49d34ff7..66f15b0d 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -104,6 +104,12 @@ module.exports = (sequelize) => { defaultValue: false, allowNull: false }, + roleIds: { + field: 'role_ids', + type: Sequelize.ARRAY({ + type: Sequelize.UUID + }) + }, createdBy: { field: 'created_by', type: Sequelize.UUID, diff --git a/src/models/Role.js b/src/models/Role.js new file mode 100644 index 00000000..57cd5025 --- /dev/null +++ b/src/models/Role.js @@ -0,0 +1,165 @@ +const { Sequelize, Model } = require('sequelize') +const config = require('config') +const errors = require('../common/errors') + +module.exports = (sequelize) => { + class Role extends Model { + /** + * Get role by id + * @param {String} id the role id + * @returns {Role} the role instance + */ + static async findById (id) { + const role = await Role.findOne({ + where: { + id + } + }) + if (!role) { + throw new errors.NotFoundError(`id: ${id} "Role" doesn't exists.`) + } + return role + } + } + Role.init( + { + id: { + type: Sequelize.UUID, + primaryKey: true, + allowNull: false, + defaultValue: Sequelize.UUIDV4 + }, + name: { + type: Sequelize.STRING(50), + allowNull: false + }, + description: { + type: Sequelize.STRING(1000) + }, + listOfSkills: { + field: 'list_of_skills', + type: Sequelize.ARRAY({ + type: Sequelize.STRING(50) + }) + }, + rates: { + type: Sequelize.ARRAY({ + type: Sequelize.JSONB({ + global: { + type: Sequelize.SMALLINT, + allowNull: false + }, + inCountry: { + field: 'in_country', + type: Sequelize.SMALLINT, + allowNull: false + }, + offShore: { + field: 'off_shore', + type: Sequelize.SMALLINT, + allowNull: false + }, + rate30Global: { + field: 'rate30_global', + type: Sequelize.SMALLINT + }, + rate30InCountry: { + field: 'rate30_in_country', + type: Sequelize.SMALLINT + }, + rate30OffShore: { + field: 'rate30_off_shore', + type: Sequelize.SMALLINT + }, + rate20Global: { + field: 'rate20_global', + type: Sequelize.SMALLINT + }, + rate20InCountry: { + field: 'rate20_in_country', + type: Sequelize.SMALLINT + }, + rate20OffShore: { + field: 'rate20_off_shore', + type: Sequelize.SMALLINT + } + }), + allowNull: false + }), + allowNull: false + }, + numberOfMembers: { + field: 'number_of_members', + type: Sequelize.NUMERIC + }, + numberOfMembersAvailable: { + field: 'number_of_members_available', + type: Sequelize.SMALLINT + }, + imageUrl: { + field: 'image_url', + type: Sequelize.STRING(255) + }, + timeToCandidate: { + field: 'time_to_candidate', + type: Sequelize.SMALLINT + }, + timeToInterview: { + field: 'time_to_interview', + type: Sequelize.SMALLINT + }, + 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: 'roles', + paranoid: true, + deletedAt: 'deletedAt', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + timestamps: true, + defaultScope: { + attributes: { + exclude: ['deletedAt'] + } + }, + hooks: { + afterCreate: (role) => { + delete role.dataValues.deletedAt + } + }, + indexes: [ + { + unique: true, + fields: ['name'], + where: { + deleted_at: null + } + } + ] + } + ) + + return Role +} diff --git a/src/routes/RoleRoutes.js b/src/routes/RoleRoutes.js new file mode 100644 index 00000000..2fb6d55b --- /dev/null +++ b/src/routes/RoleRoutes.js @@ -0,0 +1,41 @@ +/** + * Contains role routes + */ +const constants = require('../../app-constants') + +module.exports = { + '/roles': { + post: { + controller: 'RoleController', + method: 'createRole', + auth: 'jwt', + scopes: [constants.Scopes.CREATE_ROLE, constants.Scopes.ALL_ROLE] + }, + get: { + controller: 'RoleController', + method: 'searchRoles', + auth: 'jwt', + scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] + } + }, + '/roles/:id': { + get: { + controller: 'RoleController', + method: 'getRole', + auth: 'jwt', + scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] + }, + patch: { + controller: 'RoleController', + method: 'updateRole', + auth: 'jwt', + scopes: [constants.Scopes.UPDATE_ROLE, constants.Scopes.ALL_ROLE] + }, + delete: { + controller: 'RoleController', + method: 'deleteRole', + auth: 'jwt', + scopes: [constants.Scopes.DELETE_ROLE, constants.Scopes.ALL_ROLE] + } + } +} diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index 9bbe25c6..07d777d4 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -1,7 +1,7 @@ /** * Contains taas team routes */ -const constants = require('../../app-constants'); +const constants = require('../../app-constants') module.exports = { '/taas-teams': { @@ -9,85 +9,85 @@ module.exports = { controller: 'TeamController', method: 'searchTeams', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/email': { post: { controller: 'TeamController', method: 'sendEmail', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/skills': { get: { controller: 'SkillController', method: 'searchSkills', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/me': { get: { controller: 'TeamController', method: 'getMe', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id': { get: { controller: 'TeamController', method: 'getTeam', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/jobs/:jobId': { get: { controller: 'TeamController', method: 'getTeamJob', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/members': { post: { controller: 'TeamController', method: 'addMembers', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], + scopes: [constants.Scopes.READ_TAAS_TEAM] }, get: { controller: 'TeamController', method: 'searchMembers', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/invites': { get: { controller: 'TeamController', method: 'searchInvites', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/:id/members/:projectMemberId': { delete: { controller: 'TeamController', method: 'deleteMember', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, + scopes: [constants.Scopes.READ_TAAS_TEAM] + } }, '/taas-teams/createTeamRequest': { post: { controller: 'TeamController', method: 'createProj', auth: 'jwt', - scopes: [constants.Scopes.READ_TAAS_TEAM], - }, - }, -}; + scopes: [constants.Scopes.READ_TAAS_TEAM] + } + } +} diff --git a/src/services/InterviewService.js b/src/services/InterviewService.js index 10a065f4..a69a788c 100644 --- a/src/services/InterviewService.js +++ b/src/services/InterviewService.js @@ -241,8 +241,8 @@ async function requestInterview (currentUser, jobCandidateId, interview) { 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] + var foundGuestMember = _.find(guestMembers, function (guestMember) { return guestEmail === guestMember.email }) + return (foundGuestMember !== undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split('@')[0] }) try { diff --git a/src/services/JobService.js b/src/services/JobService.js index 7d855bd0..be5dfdec 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -74,6 +74,27 @@ async function _validateSkills (skills) { } } +/** + * Validate if all roles exist. + * + * @param {Array} roles the list of roles + * @returns {undefined} + */ +async function _validateRoles (roles) { + const foundRolesObj = await models.Role.findAll({ + where: { + id: roles + }, + attributes: ['id'], + raw: true + }) + const foundRoles = _.map(foundRolesObj, 'id') + const nonexistentRoles = _.difference(roles, foundRoles) + if (nonexistentRoles.length > 0) { + throw new errors.BadRequestError(`Invalid roles: [${nonexistentRoles}]`) + } +} + /** * Check user permission for getting job. * @@ -154,6 +175,10 @@ async function createJob (currentUser, job) { } await _validateSkills(job.skills) + if (job.roleIds) { + job.roleIds = _.uniq(job.roleIds) + await _validateRoles(job.roleIds) + } job.id = uuid() job.createdBy = await helper.getUserId(currentUser.userId) @@ -177,7 +202,8 @@ createJob.schema = Joi.object().keys({ rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()).required(), - isApplicationPageActive: Joi.boolean() + isApplicationPageActive: Joi.boolean(), + roleIds: Joi.array().items(Joi.string().uuid().required()) }).required() }).required() @@ -192,6 +218,10 @@ async function updateJob (currentUser, id, data) { if (data.skills) { await _validateSkills(data.skills) } + if (data.roleIds) { + data.roleIds = _.uniq(data.roleIds) + await _validateRoles(data.roleIds) + } let job = await Job.findById(id) const oldValue = job.toJSON() @@ -245,7 +275,8 @@ partiallyUpdateJob.schema = Joi.object().keys({ rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()), - isApplicationPageActive: Joi.boolean() + isApplicationPageActive: Joi.boolean(), + roleIds: Joi.array().items(Joi.string().uuid().required()).allow(null) }).required() }).required() @@ -276,7 +307,8 @@ fullyUpdateJob.schema = Joi.object().keys({ workload: Joi.workload().allow(null).default(null), skills: Joi.array().items(Joi.string().uuid()).required(), status: Joi.jobStatus().default('sourcing'), - isApplicationPageActive: Joi.boolean() + isApplicationPageActive: Joi.boolean(), + roleIds: Joi.array().items(Joi.string().uuid().required()).default(null) }).required() }).required() @@ -444,9 +476,9 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } [Op.like]: `%${criteria.title}%` } } - if (criteria.skills) { + if (criteria.skill) { filter.skills = { - [Op.contains]: [criteria.skills] + [Op.contains]: [criteria.skill] } } const jobs = await Job.findAll({ diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index f5c40206..fd3d7773 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unreachable */ /** * This service provides operations of ResourceBooking. */ diff --git a/src/services/RoleService.js b/src/services/RoleService.js new file mode 100644 index 00000000..19006f67 --- /dev/null +++ b/src/services/RoleService.js @@ -0,0 +1,305 @@ +/** + * This service provides operations of Roles. + */ + +const _ = require('lodash') +const config = require('config') +const Joi = require('joi') +const { Op } = require('sequelize') +const uuid = require('uuid') +const helper = require('../common/helper') +const logger = require('../common/logger') +const errors = require('../common/errors') +const models = require('../models') + +const Role = models.Role +const esClient = helper.getESClient() + +/** + * Check user permission for deleting, creating or updating role. + * @param {Object} currentUser the user who perform this operation. + * @returns {undefined} + */ +async function _checkUserPermissionForWriteDeleteRole (currentUser) { + if (!currentUser.hasManagePermission && !currentUser.isMachine) { + throw new errors.ForbiddenError('You are not allowed to perform this action!') + } +} + +/** + * Cleans and validates skill names using skills service + * @param {Array} skills array of skill names to validate + * @returns {undefined} + */ +async function _cleanAndValidateSkillNames (skills) { + // remove duplicates, leading and trailing whitespaces, remove empties and convert to lowercase. + const cleanedSkills = _.uniq(_.filter(_.map(skills, skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill))) + if (cleanedSkills.length > 0) { + // search skills if they are exists + const { result } = await helper.getTopcoderSkills({ name: _.join(cleanedSkills, ',') }) + const skillNames = _.map(result, 'name') + // find skills that not valid + const unValidSkills = _.differenceWith(cleanedSkills, skillNames, (a, b) => _.toLower(a) === _.toLower(b)) + if (unValidSkills.length > 0) { + throw new errors.BadRequestError(`skills: "${unValidSkills}" are not valid`) + } + return cleanedSkills + } else { + return null + } +} + +/** + * Check user permission for deleting, creating or updating role. + * @param {Object} currentUser the user who perform this operation. + * @returns {undefined} + */ +async function _checkIfSameNamedRoleExists (roleName) { + // We can't create another Role with the same name + const role = await Role.findOne({ + where: { + name: { [Op.iLike]: roleName } + }, + raw: true + }) + if (role) { + throw new errors.BadRequestError(`Role: "${role.name}" is already exists.`) + } +} + +/** + * Get role by id + * @param {Object} currentUser the user who perform this operation. + * @param {String} id the role id + * @param {Boolean} fromDb flag if query db for data or not + * @returns {Object} the role + */ +async function getRole (currentUser, id, fromDb = false) { + if (!fromDb) { + try { + const role = await esClient.get({ + index: config.esConfig.ES_INDEX_ROLE, + id + }) + return { id: role.body._id, ...role.body._source } + } catch (err) { + if (helper.isDocumentMissingException(err)) { + throw new errors.NotFoundError(`id: ${id} "Role" not found`) + } + } + } + logger.info({ component: 'RoleService', context: 'getRole', message: 'try to query db for data' }) + const role = await Role.findById(id) + + return role.toJSON() +} + +getRole.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.string().uuid().required(), + fromDb: Joi.boolean() +}).required() + +/** + * Create role + * @param {Object} currentUser the user who perform this operation + * @param {Object} role the role to be created + * @returns {Object} the created role + */ +async function createRole (currentUser, role) { + // check permission + await _checkUserPermissionForWriteDeleteRole(currentUser) + // check if another Role with the same name exists. + await _checkIfSameNamedRoleExists(role.name) + // clean and validate skill names + if (role.listOfSkills) { + role.listOfSkills = await _cleanAndValidateSkillNames(role.listOfSkills) + } + + role.id = uuid.v4() + role.createdBy = await helper.getUserId(currentUser.userId) + + const created = await Role.create(role) + + await helper.postEvent(config.TAAS_ROLE_CREATE_TOPIC, created.toJSON()) + return created.toJSON() +} + +createRole.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + role: Joi.object().keys({ + name: Joi.string().max(50).required(), + description: Joi.string().max(1000), + listOfSkills: Joi.array().items(Joi.string().max(50).required()), + rates: Joi.array().items(Joi.object().keys({ + global: Joi.smallint().required(), + inCountry: Joi.smallint().required(), + offShore: Joi.smallint().required(), + rate30Global: Joi.smallint(), + rate30InCountry: Joi.smallint(), + rate30OffShore: Joi.smallint(), + rate20Global: Joi.smallint(), + rate20InCountry: Joi.smallint(), + rate20OffShore: Joi.smallint() + }).required()).required(), + numberOfMembers: Joi.number(), + numberOfMembersAvailable: Joi.smallint(), + imageUrl: Joi.string().uri().max(255), + timeToCandidate: Joi.smallint(), + timeToInterview: Joi.smallint() + }).required() +}).required() + +/** + * Partially Update role + * @param {Object} currentUser the user who perform this operation + * @param {String} id the role id + * @param {Object} data the data to be updated + * @returns {Object} the updated role + */ +async function updateRole (currentUser, id, data) { + // check permission + await _checkUserPermissionForWriteDeleteRole(currentUser) + + const role = await Role.findById(id) + const oldValue = role.toJSON() + // if name is changed, check if another Role with the same name exists. + if (data.name && data.name.toLowerCase() !== role.dataValues.name.toLowerCase()) { + await _checkIfSameNamedRoleExists(data.name) + } + // clean and validate skill names + if (data.listOfSkills) { + data.listOfSkills = await _cleanAndValidateSkillNames(data.listOfSkills) + } + + data.updatedBy = await helper.getUserId(currentUser.userId) + const updated = await role.update(data) + + await helper.postEvent(config.TAAS_ROLE_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) + return updated.toJSON() +} + +updateRole.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.string().uuid().required(), + data: Joi.object().keys({ + name: Joi.string().max(50), + description: Joi.string().max(1000).allow(null), + listOfSkills: Joi.array().items(Joi.string().max(50).required()).allow(null), + rates: Joi.array().items(Joi.object().keys({ + global: Joi.smallint().required(), + inCountry: Joi.smallint().required(), + offShore: Joi.smallint().required(), + rate30Global: Joi.smallint(), + rate30InCountry: Joi.smallint(), + rate30OffShore: Joi.smallint(), + rate20Global: Joi.smallint(), + rate20InCountry: Joi.smallint(), + rate20OffShore: Joi.smallint() + }).required()), + numberOfMembers: Joi.number().allow(null), + numberOfMembersAvailable: Joi.smallint().allow(null), + imageUrl: Joi.string().uri().max(255).allow(null), + timeToCandidate: Joi.smallint().allow(null), + timeToInterview: Joi.smallint().allow(null) + }).required() +}).required() + +/** + * Delete role by id + * @param {Object} currentUser the user who perform this operation + * @param {String} id the role id + */ +async function deleteRole (currentUser, id) { + // check permission + await _checkUserPermissionForWriteDeleteRole(currentUser) + + const role = await Role.findById(id) + await role.destroy() + await helper.postEvent(config.TAAS_ROLE_DELETE_TOPIC, { id }) +} + +deleteRole.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.string().uuid().required() +}).required() + +/** + * List roles + * @param {Object} currentUser the user who perform this operation. + * @param {Object} criteria the search criteria + * @returns {Object} the search result + */ +async function searchRoles (currentUser, criteria) { + // clean skill names and convert into an array + criteria.skillsList = _.filter(_.map(_.split(_.trim(criteria.skillsList), ','), skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill)) + try { + const esQuery = { + index: config.get('esConfig.ES_INDEX_ROLE'), + body: { + query: { + bool: { + must: [] + } + }, + size: 10000, + sort: [{ name: { order: 'asc' } }] + } + } + // Apply skill name filters. listOfSkills array should include all skills provided in criteria. + _.each(criteria.skillsList, skill => { + esQuery.body.query.bool.must.push({ + term: { + listOfSkills: skill + } + }) + }) + // Apply name filter, allow partial match + if (criteria.keyword) { + esQuery.body.query.bool.must.push({ + wildcard: { + name: `*${criteria.keyword}*` + + } + }) + } + logger.debug({ component: 'RoleService', context: 'searchRoles', message: `Query: ${JSON.stringify(esQuery)}` }) + + const { body } = await esClient.search(esQuery) + return _.map(body.hits.hits, (hit) => _.assign(hit._source, { id: hit._id })) + } catch (err) { + logger.logFullError(err, { component: 'RoleService', context: 'searchRoles' }) + } + logger.info({ component: 'RoleService', context: 'searchRoles', message: 'fallback to DB query' }) + const filter = { [Op.and]: [] } + // Apply skill name filters. listOfSkills array should include all skills provided in criteria. + if (criteria.skillsList) { + filter[Op.and].push({ listOfSkills: { [Op.contains]: criteria.skillsList } }) + } + // Apply name filter, allow partial match and ignore case + if (criteria.keyword) { + filter[Op.and].push({ name: { [Op.iLike]: `%${criteria.keyword}%` } }) + } + const queryCriteria = { + where: filter, + order: [['name', 'asc']] + } + const roles = await Role.findAll(queryCriteria) + return roles +} + +searchRoles.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + criteria: Joi.object().keys({ + skillsList: Joi.string(), + keyword: Joi.string() + }).required() +}).required() + +module.exports = { + getRole, + createRole, + updateRole, + deleteRole, + searchRoles +} diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 3f6dbfd3..4052e942 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -2,16 +2,16 @@ * This service provides operations of Job. */ -const _ = require('lodash'); -const Joi = require('joi'); -const dateFNS = require('date-fns'); -const config = require('config'); -const emailTemplateConfig = require('../../config/email_template.config'); -const helper = require('../common/helper'); -const logger = require('../common/logger'); -const errors = require('../common/errors'); -const JobService = require('./JobService'); -const ResourceBookingService = require('./ResourceBookingService'); +const _ = require('lodash') +const Joi = require('joi') +const dateFNS = require('date-fns') +const config = require('config') +const emailTemplateConfig = require('../../config/email_template.config') +const helper = require('../common/helper') +const logger = require('../common/logger') +const errors = require('../common/errors') +const JobService = require('./JobService') +const ResourceBookingService = require('./ResourceBookingService') const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { return { @@ -20,9 +20,9 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { from: template.from, recipients: template.recipients, cc: template.cc, - sendgridTemplateId: template.sendgridTemplateId, - }; -}); + sendgridTemplateId: template.sendgridTemplateId + } +}) /** * Function to get placed resource bookings with specific projectIds @@ -30,14 +30,14 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { * @param {Array} projectIds project ids * @returns the request result */ -async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { - const criteria = { status: 'placed', projectIds }; +async function _getPlacedResourceBookingsByProjectIds (currentUser, projectIds) { + const criteria = { status: 'placed', projectIds } const { result } = await ResourceBookingService.searchResourceBookings( currentUser, criteria, { returnAll: true } - ); - return result; + ) + return result } /** @@ -46,13 +46,13 @@ async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { * @param {Array} projectIds project ids * @returns the request result */ -async function _getJobsByProjectIds(currentUser, projectIds) { +async function _getJobsByProjectIds (currentUser, projectIds) { const { result } = await JobService.searchJobs( currentUser, { projectIds }, { returnAll: true } - ); - return result; + ) + return result } /** @@ -61,26 +61,26 @@ async function _getJobsByProjectIds(currentUser, projectIds) { * @param {Object} criteria the search criteria * @returns {Object} the search result, contain total/page/perPage and result array */ -async function searchTeams(currentUser, criteria) { - const sort = `${criteria.sortBy} ${criteria.sortOrder}`; +async function searchTeams (currentUser, criteria) { + const sort = `${criteria.sortBy} ${criteria.sortOrder}` // Get projects from /v5/projects with searching criteria const { total, page, perPage, - result: projects, + result: projects } = await helper.getProjects(currentUser, { page: criteria.page, perPage: criteria.perPage, name: criteria.name, - sort, - }); + sort + }) return { total, page, perPage, - result: await getTeamDetail(currentUser, projects), - }; + result: await getTeamDetail(currentUser, projects) + } } searchTeams.schema = Joi.object() @@ -107,13 +107,13 @@ searchTeams.schema = Joi.object() then: Joi.forbidden().label( 'sortOrder(with sortBy being `best match`)' ), - otherwise: Joi.string().valid('asc', 'desc').default('desc'), + otherwise: Joi.string().valid('asc', 'desc').default('desc') }), - name: Joi.string(), + name: Joi.string() }) - .required(), + .required() }) - .required(); + .required() /** * Get team details @@ -122,69 +122,69 @@ searchTeams.schema = Joi.object() * @param {Object} isSearch the flag whether for search function * @returns {Object} the search result */ -async function getTeamDetail(currentUser, projects, isSearch = true) { - const projectIds = _.map(projects, 'id'); +async function getTeamDetail (currentUser, projects, isSearch = true) { + const projectIds = _.map(projects, 'id') // Get all placed resourceBookings filtered by projectIds const resourceBookings = await _getPlacedResourceBookingsByProjectIds( currentUser, projectIds - ); + ) // Get all jobs filtered by projectIds - const jobs = await _getJobsByProjectIds(currentUser, projectIds); + const jobs = await _getJobsByProjectIds(currentUser, projectIds) // Get first week day and last week day - const curr = new Date(); - const firstDay = dateFNS.startOfWeek(curr); - const lastDay = dateFNS.endOfWeek(curr); + const curr = new Date() + const firstDay = dateFNS.startOfWeek(curr) + const lastDay = dateFNS.endOfWeek(curr) logger.debug({ component: 'TeamService', context: 'getTeamDetail', - message: `week started: ${firstDay}, week ended: ${lastDay}`, - }); + message: `week started: ${firstDay}, week ended: ${lastDay}` + }) - const result = []; + const result = [] for (const project of projects) { - const rbs = _.filter(resourceBookings, { projectId: project.id }); - const res = _.clone(project); - res.weeklyCost = 0; - res.resources = []; + const rbs = _.filter(resourceBookings, { projectId: project.id }) + const res = _.clone(project) + res.weeklyCost = 0 + res.resources = [] if (rbs && rbs.length > 0) { // Get minimal start date and maximal end date - const startDates = []; - const endDates = []; + const startDates = [] + const endDates = [] for (const rbsItem of rbs) { if (rbsItem.startDate) { - startDates.push(new Date(rbsItem.startDate)); + startDates.push(new Date(rbsItem.startDate)) } if (rbsItem.endDate) { - endDates.push(new Date(rbsItem.endDate)); + endDates.push(new Date(rbsItem.endDate)) } } if (startDates && startDates.length > 0) { - res.startDate = _.min(startDates); + res.startDate = _.min(startDates) } if (endDates && endDates.length > 0) { - res.endDate = _.max(endDates); + res.endDate = _.max(endDates) } // Count weekly rate for (const item of rbs) { // ignore any resourceBooking that has customerRate missed if (!item.customerRate) { - continue; + continue } - const startDate = new Date(item.startDate); - const endDate = new Date(item.endDate); + const startDate = new Date(item.startDate) + const endDate = new Date(item.endDate) // normally startDate is smaller than endDate for a resourceBooking so not check if startDate < endDate if ( (!item.startDate || startDate < lastDay) && (!item.endDate || endDate > firstDay) ) { - res.weeklyCost += item.customerRate; + res.weeklyCost += item.customerRate } } @@ -194,48 +194,48 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { const resource = { id: rb.id, userId: user.id, - ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']), - }; + ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']) + } // If call function is not search, add jobId field if (!isSearch) { - resource.jobId = rb.jobId; - resource.customerRate = rb.customerRate; - resource.startDate = rb.startDate; - resource.endDate = rb.endDate; + resource.jobId = rb.jobId + resource.customerRate = rb.customerRate + resource.startDate = rb.startDate + resource.endDate = rb.endDate } - return resource; - }); + return resource + }) }) - ); + ) if (resourceInfos && resourceInfos.length > 0) { - res.resources = resourceInfos; + res.resources = resourceInfos - const userHandles = _.map(resourceInfos, 'handle'); + const userHandles = _.map(resourceInfos, 'handle') // Get user photo from /v5/members - const members = await helper.getMembers(userHandles); + const members = await helper.getMembers(userHandles) for (const item of res.resources) { const findMember = _.find(members, { - handleLower: item.handle.toLowerCase(), - }); + handleLower: item.handle.toLowerCase() + }) if (findMember && findMember.photoURL) { - item.photo_url = findMember.photoURL; + item.photo_url = findMember.photoURL } } } } - const jobsTmp = _.filter(jobs, { projectId: project.id }); + const jobsTmp = _.filter(jobs, { projectId: project.id }) if (jobsTmp && jobsTmp.length > 0) { if (isSearch) { // Count total positions - res.totalPositions = 0; + res.totalPositions = 0 for (const item of jobsTmp) { // only sum numPositions of jobs whose status is NOT cancelled or closed if (['cancelled', 'closed'].includes(item.status)) { - continue; + continue } - res.totalPositions += item.numPositions; + res.totalPositions += item.numPositions } } else { res.jobs = _.map(jobsTmp, (job) => { @@ -249,15 +249,15 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { 'skills', 'customerRate', 'status', - 'title', - ]); - }); + 'title' + ]) + }) } } - result.push(res); + result.push(res) } - return result; + return result } /** @@ -266,35 +266,35 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { * @param {String} id the job id * @returns {Object} the team */ -async function getTeam(currentUser, id) { - const project = await helper.getProjectById(currentUser, id); - const result = await getTeamDetail(currentUser, [project], false); - const teamDetail = result[0]; +async function getTeam (currentUser, id) { + const project = await helper.getProjectById(currentUser, id) + const result = await getTeamDetail(currentUser, [project], false) + const teamDetail = result[0] // add job skills for result - let jobSkills = []; + let jobSkills = [] if (teamDetail && teamDetail.jobs) { for (const job of teamDetail.jobs) { if (job.skills) { - const usersPromises = []; + const usersPromises = [] _.map(job.skills, (skillId) => { - usersPromises.push(helper.getSkillById(skillId)); - }); - jobSkills = await Promise.all(usersPromises); - job.skills = jobSkills; + usersPromises.push(helper.getSkillById(skillId)) + }) + jobSkills = await Promise.all(usersPromises) + job.skills = jobSkills } } } - return teamDetail; + return teamDetail } getTeam.schema = Joi.object() .keys({ currentUser: Joi.object().required(), - id: Joi.number().integer().required(), + id: Joi.number().integer().required() }) - .required(); + .required() /** * Get team job with id @@ -303,25 +303,25 @@ getTeam.schema = Joi.object() * @param {String} jobId the job id * @returns the team job */ -async function getTeamJob(currentUser, id, jobId) { - const project = await helper.getProjectById(currentUser, id); - const jobs = await _getJobsByProjectIds(currentUser, [project.id]); - const job = _.find(jobs, { id: jobId }); +async function getTeamJob (currentUser, id, jobId) { + const project = await helper.getProjectById(currentUser, id) + const jobs = await _getJobsByProjectIds(currentUser, [project.id]) + const job = _.find(jobs, { id: jobId }) if (!job) { throw new errors.NotFoundError( `id: ${jobId} "Job" with Team id ${id} doesn't exist` - ); + ) } const result = { id: job.id, - title: job.title, - }; + title: job.title + } if (job.skills) { result.skills = await Promise.all( _.map(job.skills, (skillId) => helper.getSkillById(skillId)) - ); + ) } // If the job has candidates, the following data for each candidate would be populated: @@ -336,12 +336,12 @@ async function getTeamJob(currentUser, id, jobId) { _.map(_.uniq(_.map(job.candidates, 'userId')), (userId) => helper.getUserById(userId, true) ) - ); - const userMap = _.groupBy(users, 'id'); + ) + const userMap = _.groupBy(users, 'id') // find photo URLs for users - const members = await helper.getMembers(_.map(users, 'handle')); - const photoURLMap = _.groupBy(members, 'handleLower'); + const members = await helper.getMembers(_.map(users, 'handle')) + const photoURLMap = _.groupBy(members, 'handleLower') result.candidates = _.map(job.candidates, (candidate) => { const candidateData = _.pick(candidate, [ @@ -349,33 +349,33 @@ async function getTeamJob(currentUser, id, jobId) { 'resume', 'userId', 'interviews', - 'id', - ]); - const userData = userMap[candidate.userId][0]; + 'id' + ]) + const userData = userMap[candidate.userId][0] // attach user data to the candidate Object.assign( candidateData, _.pick(userData, ['handle', 'firstName', 'lastName', 'skills']) - ); + ) // attach photo URL to the candidate - const handleLower = userData.handle.toLowerCase(); + const handleLower = userData.handle.toLowerCase() if (photoURLMap[handleLower]) { - candidateData.photo_url = photoURLMap[handleLower][0].photoURL; + candidateData.photo_url = photoURLMap[handleLower][0].photoURL } - return candidateData; - }); + return candidateData + }) } - return result; + return result } getTeamJob.schema = Joi.object() .keys({ currentUser: Joi.object().required(), id: Joi.number().integer().required(), - jobId: Joi.string().guid().required(), + jobId: Joi.string().guid().required() }) - .required(); + .required() /** * Send email through a particular template @@ -383,21 +383,21 @@ getTeamJob.schema = Joi.object() * @param {Object} data the email object * @returns {undefined} */ -async function sendEmail(currentUser, data) { - const template = emailTemplates[data.template]; - const dataCC = data.cc || []; - const templateCC = template.cc || []; - const dataRecipients = data.recipients || []; - const templateRecipients = template.recipients || []; +async function sendEmail (currentUser, data) { + const template = emailTemplates[data.template] + const dataCC = data.cc || [] + const templateCC = template.cc || [] + const dataRecipients = data.recipients || [] + const templateRecipients = template.recipients || [] const subjectBody = { subject: data.subject || template.subject, - body: data.body || template.body, - }; + body: data.body || template.body + } for (const key in subjectBody) { subjectBody[key] = await helper.substituteStringByObject( subjectBody[key], data.data - ); + ) } const emailData = { // override template if coming data already have the 'from' address @@ -407,9 +407,9 @@ async function sendEmail(currentUser, data) { cc: _.uniq([...dataCC, ...templateCC]), data: { ...data.data, ...subjectBody }, sendgrid_template_id: template.sendgridTemplateId, - version: 'v3', - }; - await helper.postEvent(config.EMAIL_TOPIC, emailData); + version: 'v3' + } + await helper.postEvent(config.EMAIL_TOPIC, emailData) } sendEmail.schema = Joi.object() @@ -423,11 +423,11 @@ sendEmail.schema = Joi.object() data: Joi.object().required(), from: Joi.string().email(), recipients: Joi.array().items(Joi.string().email()).allow(null), - cc: Joi.array().items(Joi.string().email()).allow(null), + cc: Joi.array().items(Joi.string().email()).allow(null) }) - .required(), + .required() }) - .required(); + .required() /** * Add a member to a team as customer. @@ -437,25 +437,25 @@ sendEmail.schema = Joi.object() * @param {String} fields the fields to be returned * @returns {Object} the member added */ -async function _addMemberToProjectAsCustomer(projectId, userId, fields) { +async function _addMemberToProjectAsCustomer (projectId, userId, fields) { try { const member = await helper.createProjectMember( projectId, { userId: userId, role: 'customer' }, { fields } - ); - return member; + ) + return member } catch (err) { - err.message = _.get(err, 'response.body.message') || err.message; + err.message = _.get(err, 'response.body.message') || err.message if (err.message && err.message.includes('User already registered')) { - throw new Error('User is already added'); + throw new Error('User is already added') } logger.error({ component: 'TeamService', context: '_addMemberToProjectAsCustomer', - message: err.message, - }); - throw err; + message: err.message + }) + throw err } } @@ -467,16 +467,16 @@ async function _addMemberToProjectAsCustomer(projectId, userId, fields) { * @param {Object} data the object including members with handle/email to be added * @returns {Object} the success/failed added members */ -async function addMembers(currentUser, id, criteria, data) { - await helper.getProjectById(currentUser, id); // check whether the user can access the project +async function addMembers (currentUser, id, criteria, data) { + await helper.getProjectById(currentUser, id) // check whether the user can access the project const result = { success: [], - failed: [], - }; + failed: [] + } - const handles = data.handles || []; - const emails = data.emails || []; + const handles = data.handles || [] + const emails = data.emails || [] const handleMembers = await helper .getMemberDetailsByHandles(handles) @@ -484,9 +484,9 @@ async function addMembers(currentUser, id, criteria, data) { _.map(members, (member) => ({ ...member, // populate members with lower-cased handle for case insensitive search - handleLowerCase: member.handle.toLowerCase(), + handleLowerCase: member.handle.toLowerCase() })) - ); + ) const emailMembers = await helper .getMemberDetailsByEmails(emails) @@ -494,20 +494,20 @@ async function addMembers(currentUser, id, criteria, data) { _.map(members, (member) => ({ ...member, // populate members with lower-cased email for case insensitive search - emailLowerCase: member.email.toLowerCase(), + emailLowerCase: member.email.toLowerCase() })) - ); + ) await Promise.all([ Promise.all( handles.map((handle) => { const memberDetails = _.find(handleMembers, { - handleLowerCase: handle.toLowerCase(), - }); + handleLowerCase: handle.toLowerCase() + }) if (!memberDetails) { - result.failed.push({ error: "User doesn't exist", handle }); - return; + result.failed.push({ error: "User doesn't exist", handle }) + return } return _addMemberToProjectAsCustomer( @@ -517,23 +517,23 @@ async function addMembers(currentUser, id, criteria, data) { ) .then((member) => { // note, that we return `handle` in the same case it was in request - result.success.push({ ...member, handle }); + result.success.push({ ...member, handle }) }) .catch((err) => { - result.failed.push({ error: err.message, handle }); - }); + result.failed.push({ error: err.message, handle }) + }) }) ), Promise.all( emails.map((email) => { const memberDetails = _.find(emailMembers, { - emailLowerCase: email.toLowerCase(), - }); + emailLowerCase: email.toLowerCase() + }) if (!memberDetails) { - result.failed.push({ error: "User doesn't exist", email }); - return; + result.failed.push({ error: "User doesn't exist", email }) + return } return _addMemberToProjectAsCustomer( @@ -543,16 +543,16 @@ async function addMembers(currentUser, id, criteria, data) { ) .then((member) => { // note, that we return `email` in the same case it was in request - result.success.push({ ...member, email }); + result.success.push({ ...member, email }) }) .catch((err) => { - result.failed.push({ error: err.message, email }); - }); + result.failed.push({ error: err.message, email }) + }) }) - ), - ]); + ) + ]) - return result; + return result } addMembers.schema = Joi.object() @@ -561,18 +561,18 @@ addMembers.schema = Joi.object() id: Joi.number().integer().required(), criteria: Joi.object() .keys({ - fields: Joi.string(), + fields: Joi.string() }) .required(), data: Joi.object() .keys({ handles: Joi.array().items(Joi.string()), - emails: Joi.array().items(Joi.string().email()), + emails: Joi.array().items(Joi.string().email()) }) .or('handles', 'emails') - .required(), + .required() }) - .required(); + .required() /** * Search members in a team. @@ -583,9 +583,9 @@ addMembers.schema = Joi.object() * @params {Object} criteria the search criteria * @returns {Object} the search result */ -async function searchMembers(currentUser, id, criteria) { - const result = await helper.listProjectMembers(currentUser, id, criteria); - return { result }; +async function searchMembers (currentUser, id, criteria) { + const result = await helper.listProjectMembers(currentUser, id, criteria) + return { result } } searchMembers.schema = Joi.object() @@ -595,11 +595,11 @@ searchMembers.schema = Joi.object() criteria: Joi.object() .keys({ role: Joi.string(), - fields: Joi.string(), + fields: Joi.string() }) - .required(), + .required() }) - .required(); + .required() /** * Search member invites for a team. @@ -610,13 +610,13 @@ searchMembers.schema = Joi.object() * @params {Object} criteria the search criteria * @returns {Object} the search result */ -async function searchInvites(currentUser, id, criteria) { +async function searchInvites (currentUser, id, criteria) { const result = await helper.listProjectMemberInvites( currentUser, id, criteria - ); - return { result }; + ) + return { result } } searchInvites.schema = Joi.object() @@ -625,11 +625,11 @@ searchInvites.schema = Joi.object() id: Joi.number().integer().required(), criteria: Joi.object() .keys({ - fields: Joi.string(), + fields: Joi.string() }) - .required(), + .required() }) - .required(); + .required() /** * Remove a member from a team. @@ -640,17 +640,17 @@ searchInvites.schema = Joi.object() * @param {String} projectMemberId the id of the project member * @returns {undefined} */ -async function deleteMember(currentUser, id, projectMemberId) { - await helper.deleteProjectMember(currentUser, id, projectMemberId); +async function deleteMember (currentUser, id, projectMemberId) { + await helper.deleteProjectMember(currentUser, id, projectMemberId) } deleteMember.schema = Joi.object() .keys({ currentUser: Joi.object().required(), id: Joi.number().integer().required(), - projectMemberId: Joi.number().integer().required(), + projectMemberId: Joi.number().integer().required() }) - .required(); + .required() /** * Return details about the current user. @@ -659,31 +659,31 @@ deleteMember.schema = Joi.object() * @params {Object} criteria the search criteria * @returns {Object} the user data for current user */ -async function getMe(currentUser) { - return helper.getUserByExternalId(currentUser.userId); +async function getMe (currentUser) { + return helper.getUserByExternalId(currentUser.userId) } getMe.schema = Joi.object() .keys({ - currentUser: Joi.object().required(), + currentUser: Joi.object().required() }) - .required(); + .required() /** * @param {Object} currentUser the user performing the operation. * @param {Object} data project data * @returns {Object} the created project */ -async function createProj(currentUser, data) { - return helper.createProject(currentUser, data); +async function createProj (currentUser, data) { + return helper.createProject(currentUser, data) } createProj.schema = Joi.object() .keys({ currentUser: Joi.object().required(), - data: Joi.object().required(), + data: Joi.object().required() }) - .required(); + .required() module.exports = { searchTeams, @@ -695,5 +695,5 @@ module.exports = { searchInvites, deleteMember, getMe, - createProj, -}; + createProj +} diff --git a/taas-apis.patch b/taas-apis.patch new file mode 100644 index 00000000..dcec96d8 --- /dev/null +++ b/taas-apis.patch @@ -0,0 +1,9418 @@ +From 79e73a98f25a56e793aaadbb9255bf3a99ecedd5 Mon Sep 17 00:00:00 2001 +From: eisbilir +Date: Sat, 29 May 2021 00:14:42 +0300 +Subject: [PATCH] role endpoint added + +--- + README.md | 6 +- + app-constants.js | 8 +- + config/default.js | 9 + + data/demo-data.json | 260 +- + ...coder-bookings-api.postman_collection.json | 3799 ++++++++++++++++- + docs/swagger.yaml | 476 +++ + ...topcoder-bookings.postman_environment.json | 56 +- + local/kafka-client/topics.txt | 3 + + migrations/2021-05-27-1-role-table-create.js | 146 + + .../2021-05-27-2-job-add-roleIds-field.js | 19 + + package.json | 1 + + scripts/data/exportData.js | 2 +- + scripts/data/importData.js | 2 +- + scripts/es/createIndex.js | 3 +- + scripts/es/deleteIndex.js | 3 +- + scripts/es/reIndexAll.js | 1 + + scripts/es/reIndexRoles.js | 37 + + src/bootstrap.js | 3 +- + src/common/helper.js | 1115 ++--- + src/controllers/RoleController.js | 59 + + src/controllers/TeamController.js | 62 +- + src/eventHandlers/RoleEventHandler.js | 64 + + src/eventHandlers/index.js | 4 +- + src/models/Job.js | 6 + + src/models/Role.js | 165 + + src/routes/RoleRoutes.js | 41 + + src/routes/TeamRoutes.js | 48 +- + src/services/InterviewService.js | 4 +- + src/services/JobService.js | 42 +- + src/services/ResourceBookingService.js | 1 + + src/services/RoleService.js | 305 ++ + src/services/TeamService.js | 390 +- + 32 files changed, 6271 insertions(+), 869 deletions(-) + create mode 100644 migrations/2021-05-27-1-role-table-create.js + create mode 100644 migrations/2021-05-27-2-job-add-roleIds-field.js + create mode 100644 scripts/es/reIndexRoles.js + create mode 100644 src/controllers/RoleController.js + create mode 100644 src/eventHandlers/RoleEventHandler.js + create mode 100644 src/models/Role.js + create mode 100644 src/routes/RoleRoutes.js + create mode 100644 src/services/RoleService.js + +diff --git a/README.md b/README.md +index 5e3895c..aa36c62 100644 +--- a/README.md ++++ b/README.md +@@ -87,6 +87,9 @@ + tc-taas-es-processor | [2021-04-09T21:20:19.035Z] app INFO : Starting kafka consumer + tc-taas-es-processor | 2021-04-09T21:20:21.292Z INFO no-kafka-client Joined group taas-es-processor generationId 1 as no-kafka-client-076538fc-60dd-4ca4-a2b9-520bdf73bc9e + tc-taas-es-processor | 2021-04-09T21:20:21.293Z INFO no-kafka-client Elected as group leader ++ tc-taas-es-processor | 2021-04-09T21:20:21.449Z DEBUG no-kafka-client Subscribed to taas.role.update:0 offset 0 leader kafka:9093 ++ tc-taas-es-processor | 2021-04-09T21:20:21.450Z DEBUG no-kafka-client Subscribed to taas.role.delete:0 offset 0 leader kafka:9093 ++ tc-taas-es-processor | 2021-04-09T21:20:21.451Z DEBUG no-kafka-client Subscribed to taas.role.requested:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-04-09T21:20:21.452Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.create:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-04-09T21:20:21.455Z DEBUG no-kafka-client Subscribed to taas.job.create:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-04-09T21:20:21.456Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.delete:0 offset 0 leader kafka:9093 +@@ -103,7 +106,7 @@ + tc-taas-es-processor | 2021-04-09T21:20:21.473Z DEBUG no-kafka-client Subscribed to taas.job.update:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-04-09T21:20:21.474Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.update:0 offset 0 leader kafka:9093 + tc-taas-es-processor | [2021-04-09T21:20:21.475Z] app INFO : Initialized....... +- tc-taas-es-processor | [2021-04-09T21:20:21.479Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete,taas.workperiod.create,taas.workperiod.update,taas.workperiod.delete,taas.workperiodpayment.create,taas.workperiodpayment.update,taas.workperiodpayment.delete ++ tc-taas-es-processor | [2021-04-09T21:20:21.479Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete,taas.workperiod.create,taas.workperiod.update,taas.workperiod.delete,taas.workperiodpayment.create,taas.workperiodpayment.update,taas.interview.requested,taas.interview.update,taas.interview.bulkUpdate,taas.role.requested,taas.role.update,taas.role.delete + tc-taas-es-processor | [2021-04-09T21:20:21.480Z] app INFO : Kick Start....... + tc-taas-es-processor | ********** Topcoder Health Check DropIn listening on port 3001 + tc-taas-es-processor | Topcoder Health Check DropIn started and ready to roll +@@ -194,6 +197,7 @@ To be able to change and test `taas-es-processor` locally you can follow the nex + | `npm run index:jobs ` | Indexes job data from db into ES, if jobId is not given all data is indexed. Use `-- --force` flag to skip confirmation | + | `npm run index:job-candidates ` | Indexes job candidate data from db into ES, if jobCandidateId is not given all data is indexed. Use `-- --force` flag to skip confirmation | + | `npm run index:resource-bookings ` | Indexes resource bookings data from db into ES, if resourceBookingsId is not given all data is indexed. Use `-- --force` flag to skip confirmation | ++| `npm run index:roles ` | Indexes roles data from db into ES, if roleId is not given all data is indexed. Use `-- --force` flag to skip confirmation | + | `npm run services:up` | Start services via docker-compose for local development. | + | `npm run services:down` | Stop services via docker-compose for local development. | + | `npm run services:logs -- -f ` | View logs of some service inside docker-compose. | +diff --git a/app-constants.js b/app-constants.js +index 534e46d..9b57772 100644 +--- a/app-constants.js ++++ b/app-constants.js +@@ -49,7 +49,13 @@ const Scopes = { + READ_INTERVIEW: 'read:taas-interviews', + CREATE_INTERVIEW: 'create:taas-interviews', + UPDATE_INTERVIEW: 'update:taas-interviews', +- ALL_INTERVIEW: 'all:taas-interviews' ++ ALL_INTERVIEW: 'all:taas-interviews', ++ // role ++ READ_ROLE: 'read:taas-roles', ++ CREATE_ROLE: 'create:taas-roles', ++ UPDATE_ROLE: 'update:taas-roles', ++ DELETE_ROLE: 'delete:taas-roles', ++ ALL_ROLE: 'all:taas-roles' + } + + // Interview related constants +diff --git a/config/default.js b/config/default.js +index 2b5ca7b..cf2a8a4 100644 +--- a/config/default.js ++++ b/config/default.js +@@ -76,6 +76,8 @@ module.exports = { + ES_INDEX_JOB_CANDIDATE: process.env.ES_INDEX_JOB_CANDIDATE || 'job_candidate', + // the resource booking index + ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking', ++ // the role index ++ ES_INDEX_ROLE: process.env.ES_INDEX_ROLE || 'role', + + // the max bulk size in MB for ES indexing + MAX_BULK_REQUEST_SIZE_MB: process.env.MAX_BULK_REQUEST_SIZE_MB || 20, +@@ -131,6 +133,13 @@ module.exports = { + TAAS_INTERVIEW_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_UPDATE_TOPIC || 'taas.interview.update', + // the interview bulk update Kafka message topic + TAAS_INTERVIEW_BULK_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_BULK_UPDATE_TOPIC || 'taas.interview.bulkUpdate', ++ // topics for role service ++ // the create role entity Kafka message topic ++ TAAS_ROLE_CREATE_TOPIC: process.env.TAAS_ROLE_CREATE_TOPIC || 'taas.role.requested', ++ // the update role entity Kafka message topic ++ TAAS_ROLE_UPDATE_TOPIC: process.env.TAAS_ROLE_UPDATE_TOPIC || 'taas.role.update', ++ // the delete role entity Kafka message topic ++ TAAS_ROLE_DELETE_TOPIC: process.env.TAAS_ROLE_DELETE_TOPIC || 'taas.role.delete', + + // the Kafka message topic for sending email + EMAIL_TOPIC: process.env.EMAIL_TOPIC || 'external.action.email', +diff --git a/data/demo-data.json b/data/demo-data.json +index e073344..5f6c4c0 100644 +--- a/data/demo-data.json ++++ b/data/demo-data.json +@@ -20,6 +20,7 @@ + ], + "status": "in-review", + "isApplicationPageActive": false, ++ "roleIds": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:21:10.394Z", +@@ -45,6 +46,7 @@ + ], + "status": "in-review", + "isApplicationPageActive": false, ++ "roleIds": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:11:26.934Z", +@@ -70,6 +72,7 @@ + ], + "status": "in-review", + "isApplicationPageActive": false, ++ "roleIds": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:23:18.595Z", +@@ -95,6 +98,7 @@ + ], + "status": "in-review", + "isApplicationPageActive": false, ++ "roleIds": null, + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-09T21:12:09.293Z", +@@ -181,18 +185,29 @@ + "interviews": [ + { + "id": "077aa2ca-5b60-4ad9-a965-1b37e08a5046", ++ "xaiId": null, + "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", +- "googleCalendarId": null, +- "customMessage": null, +- "xaiTemplate": "interview-30", ++ "calendarEventId": null, ++ "templateUrl": "interview-30", ++ "templateId": null, ++ "templateType": null, ++ "title": null, ++ "locationDetails": null, ++ "duration": null, + "round": 1, + "startTimestamp": null, +- "attendeesList": null, ++ "endTimestamp": null, ++ "hostName": null, ++ "hostEmail": null, ++ "guestNames": null, ++ "guestEmails": null, + "status": "Completed", ++ "rescheduleUrl": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:16:10.887Z", +- "updatedAt": "2021-05-09T21:16:10.887Z" ++ "updatedAt": "2021-05-09T21:16:10.887Z", ++ "deletedAt": null + } + ] + }, +@@ -210,33 +225,55 @@ + "interviews": [ + { + "id": "b1f7ba76-640f-47e2-9463-59e51b51ec60", ++ "xaiId": null, + "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", +- "googleCalendarId": "dummyId", +- "customMessage": "This is a custom message", +- "xaiTemplate": "interview-30", ++ "calendarEventId": null, ++ "templateUrl": "interview-30", ++ "templateId": null, ++ "templateType": null, ++ "title": null, ++ "locationDetails": null, ++ "duration": null, + "round": 2, + "startTimestamp": null, +- "attendeesList": null, ++ "endTimestamp": null, ++ "hostName": null, ++ "hostEmail": null, ++ "guestNames": null, ++ "guestEmails": null, + "status": "Scheduling", ++ "rescheduleUrl": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:17:23.517Z", +- "updatedAt": "2021-05-09T21:17:23.517Z" ++ "updatedAt": "2021-05-09T21:17:23.517Z", ++ "deletedAt": null + }, + { + "id": "3144fa65-ea1a-4bec-81b0-7cb1c8845826", ++ "xaiId": null, + "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", +- "googleCalendarId": null, +- "customMessage": null, +- "xaiTemplate": "interview-30", ++ "calendarEventId": null, ++ "templateUrl": "interview-30", ++ "templateId": null, ++ "templateType": null, ++ "title": null, ++ "locationDetails": null, ++ "duration": null, + "round": 1, + "startTimestamp": null, +- "attendeesList": null, ++ "endTimestamp": null, ++ "hostName": null, ++ "hostEmail": null, ++ "guestNames": null, ++ "guestEmails": null, + "status": "Completed", ++ "rescheduleUrl": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:16:39.019Z", +- "updatedAt": "2021-05-09T21:16:39.019Z" ++ "updatedAt": "2021-05-09T21:16:39.019Z", ++ "deletedAt": null + } + ] + }, +@@ -254,54 +291,81 @@ + "interviews": [ + { + "id": "976d23a9-5710-453f-99d9-f57a588bb610", ++ "xaiId": null, + "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", +- "googleCalendarId": "dummyId", +- "customMessage": "This is a custom message", +- "xaiTemplate": "interview-30", ++ "calendarEventId": null, ++ "templateUrl": "interview-30", ++ "templateId": null, ++ "templateType": null, ++ "title": null, ++ "locationDetails": null, ++ "duration": null, + "round": 3, + "startTimestamp": null, +- "attendeesList": [ +- "attendee1@yopmail.com", +- "attendee2@yopmail.com" +- ], ++ "endTimestamp": null, ++ "hostName": null, ++ "hostEmail": null, ++ "guestNames": null, ++ "guestEmails": null, + "status": "Scheduling", ++ "rescheduleUrl": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:21:28.713Z", +- "updatedAt": "2021-05-09T21:21:28.713Z" ++ "updatedAt": "2021-05-09T21:21:28.713Z", ++ "deletedAt": null + }, + { + "id": "a23e1bf2-1084-4cfe-a0d8-d83bc6fec655", ++ "xaiId": null, + "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", +- "googleCalendarId": "dummyId", +- "customMessage": "This is a custom message", +- "xaiTemplate": "interview-30", ++ "calendarEventId": null, ++ "templateUrl": "interview-30", ++ "templateId": null, ++ "templateType": null, ++ "title": null, ++ "locationDetails": null, ++ "duration": null, + "round": 2, + "startTimestamp": null, +- "attendeesList": [ +- "attendee1@yopmail.com", +- "attendee2@yopmail.com" +- ], ++ "endTimestamp": null, ++ "hostName": null, ++ "hostEmail": null, ++ "guestNames": null, ++ "guestEmails": null, + "status": "Scheduling", ++ "rescheduleUrl": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:21:22.428Z", +- "updatedAt": "2021-05-09T21:21:22.428Z" ++ "updatedAt": "2021-05-09T21:21:22.428Z", ++ "deletedAt": null + }, + { + "id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb", ++ "xaiId": null, + "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", +- "googleCalendarId": null, +- "customMessage": null, +- "xaiTemplate": "interview-30", ++ "calendarEventId": null, ++ "templateUrl": "interview-30", ++ "templateId": null, ++ "templateType": null, ++ "title": null, ++ "locationDetails": null, ++ "duration": null, + "round": 1, + "startTimestamp": null, +- "attendeesList": null, ++ "endTimestamp": null, ++ "hostName": null, ++ "hostEmail": null, ++ "guestNames": null, ++ "guestEmails": null, + "status": "Completed", ++ "rescheduleUrl": null, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:21:17.346Z", +- "updatedAt": "2021-05-09T21:21:17.346Z" ++ "updatedAt": "2021-05-09T21:21:17.346Z", ++ "deletedAt": null + } + ] + }, +@@ -2052,5 +2116,127 @@ + } + ] + } ++ ], ++ "Role": [ ++ { ++ "id": "c145247d-5757-463d-9317-ff9e7026d403", ++ "name": "Angular Developer", ++ "description": "Angular is an open-source, client-side framework based on TypeScript and designed for building web applications.", ++ "listOfSkills": [ ++ "database", ++ "winforms", ++ "user interface (ui)", ++ "photoshop" ++ ], ++ "rates": [ ++ { ++ "global": 50, ++ "offShore": 10, ++ "inCountry": 20 ++ }, ++ { ++ "global": 25, ++ "offShore": 5, ++ "inCountry": 15 ++ } ++ ], ++ "numberOfMembers": "10", ++ "numberOfMembersAvailable": 8, ++ "imageUrl": "http://images.topcoder.com/member", ++ "timeToCandidate": 105, ++ "timeToInterview": 100, ++ "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", ++ "updatedBy": null, ++ "createdAt": "2021-05-27T21:43:08.201Z", ++ "updatedAt": "2021-05-27T21:43:08.201Z" ++ }, ++ { ++ "id": "d7ff0289-d3ea-44d8-b39a-53bba5b5b309", ++ "name": "Dev Ops Engineer", ++ "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.", ++ "listOfSkills": [ ++ "dropwizard", ++ "nginx", ++ "machine learning", ++ "force.com" ++ ], ++ "rates": [ ++ { ++ "global": 50, ++ "offShore": 10, ++ "inCountry": 20, ++ "rate20Global": 20, ++ "rate30Global": 20, ++ "rate20OffShore": 35, ++ "rate30OffShore": 35, ++ "rate20InCountry": 15, ++ "rate30InCountry": 15 ++ }, ++ { ++ "global": 25, ++ "offShore": 5, ++ "inCountry": 15, ++ "rate20Global": 20, ++ "rate30Global": 20, ++ "rate20OffShore": 35, ++ "rate30OffShore": 35, ++ "rate20InCountry": 15, ++ "rate30InCountry": 15 ++ } ++ ], ++ "numberOfMembers": "10", ++ "numberOfMembersAvailable": 8, ++ "imageUrl": "http://images.topcoder.com/member", ++ "timeToCandidate": 105, ++ "timeToInterview": 100, ++ "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", ++ "updatedBy": null, ++ "createdAt": "2021-05-27T21:43:04.717Z", ++ "updatedAt": "2021-05-27T21:43:04.717Z" ++ }, ++ { ++ "id": "e7b7e818-40d4-4102-b486-09bdd21400b8", ++ "name": "Salesforce Developer", ++ "description": "A Salesforce developer is a programmer who builds Salesforce applications across various PaaS (Platform as a Service) platforms.", ++ "listOfSkills": [ ++ "docker", ++ ".net", ++ "appcelerator", ++ "flux" ++ ], ++ "rates": [ ++ { ++ "global": 50, ++ "offShore": 10, ++ "inCountry": 20, ++ "rate20Global": 20, ++ "rate30Global": 20, ++ "rate20OffShore": 35, ++ "rate30OffShore": 35, ++ "rate20InCountry": 15, ++ "rate30InCountry": 15 ++ }, ++ { ++ "global": 25, ++ "offShore": 5, ++ "inCountry": 15, ++ "rate20Global": 20, ++ "rate30Global": 20, ++ "rate20OffShore": 35, ++ "rate30OffShore": 35, ++ "rate20InCountry": 15, ++ "rate30InCountry": 15 ++ } ++ ], ++ "numberOfMembers": "10", ++ "numberOfMembersAvailable": 6, ++ "imageUrl": "http://images.topcoder.com/member", ++ "timeToCandidate": 105, ++ "timeToInterview": 100, ++ "createdBy": "00000000-0000-0000-0000-000000000000", ++ "updatedBy": null, ++ "createdAt": "2021-05-27T21:43:09.342Z", ++ "updatedAt": "2021-05-27T21:43:09.342Z" ++ } + ] +-} ++} +\ No newline at end of file +diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json +index a0518c5..96250f3 100644 +--- a/docs/Topcoder-bookings-api.postman_collection.json ++++ b/docs/Topcoder-bookings-api.postman_collection.json +@@ -1,6 +1,6 @@ + { + "info": { +- "_postman_id": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", ++ "_postman_id": "b0508e11-af20-4ea3-bfda-fec9f40ea531", + "name": "Topcoder-bookings-api", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, +@@ -17816,6 +17816,2993 @@ + } + ] + }, ++ { ++ "name": "Roles", ++ "item": [ ++ { ++ "name": "Create Role", ++ "item": [ ++ { ++ "name": "create role with admin", ++ "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(\"roleId-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 ],\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}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with booking manager", ++ "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(\"roleId-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 \"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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\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}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with m2m create", ++ "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(\"roleId-3\", response.id);\r", ++ " }\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_m2m_create_role}}" ++ } ++ ], ++ "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 ],\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\": 6,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 403', function () {\r", ++ " pm.response.to.have.status(403);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 403', function () {\r", ++ " pm.response.to.have.status(403);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role 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", ++ "type": "text", ++ "value": "Bearer invalid_token" ++ } ++ ], ++ "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with existent name", ++ "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(\"Role: \\\"Dev Ops Engineer\\\" is already exists.\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with missing parameter 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(\"\\\"role.name\\\" is required\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with missing parameter 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(\"\\\"role.rates\\\" is required\")\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 ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with missing parameter 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(\"\\\"role.rates\\\" does not contain 1 required value(s)\")\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 ],\n \"rates\": [\n\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with missing parameter 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(\"\\\"role.rates[0].global\\\" is required\")\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 ],\n \"rates\": [\n {\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with missing parameter 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(\"\\\"role.rates[0].inCountry\\\" is required\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with missing parameter 6", ++ "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(\"\\\"role.rates[0].offShore\\\" is required\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 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(\"\\\"role.name\\\" length must be less than or equal to 50 characters long\")\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 Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer 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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 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(\"\\\"role.listOfSkills[0]\\\" length must be less than or equal to 50 characters long\")\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 Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard\",\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 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(\"\\\"role.listOfSkills\\\" must be an array\")\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\":\"Dropwizard\",\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 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(\"\\\"role.rates\\\" must be an array\")\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 ],\n \"rates\": {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 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(\"\\\"role.rates[0].global\\\" must be a number\")\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 ],\n \"rates\": [\n {\n \"global\": \"first\",\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 6", ++ "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(\"\\\"role.rates[0].inCountry\\\" must be a number\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": \"fifty\",\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 7", ++ "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(\"\\\"role.numberOfMembers\\\" must be a number\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": null,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 8", ++ "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(\"\\\"role.imageUrl\\\" must be a valid uri\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 9", ++ "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(\"\\\"role.timeToCandidate\\\" must be less than or equal to 32767\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 99999,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "create role with invalid parameter 10", ++ "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(\"skills: \\\"teamworking,communication,problem-solving\\\" are not valid\")\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 2\",\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 \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 55,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ }, ++ { ++ "name": "Get Role", ++ "item": [ ++ { ++ "name": "get role with admin", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "get role with booking manager 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", ++ "type": "text", ++ "value": "Bearer {{token_bookingManager}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-2}}?fromDb=true", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-2}}" ++ ], ++ "query": [ ++ { ++ "key": "fromDb", ++ "value": "true" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "get role with m2m read", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_m2m_read_role}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-3}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-3}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "get role with connect user 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", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}?fromDb=true", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ], ++ "query": [ ++ { ++ "key": "fromDb", ++ "value": "true" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "get role with member", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-2}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-2}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "get role 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": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer invalid token" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-2}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-2}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "get role with invalid id", ++ "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(\"\\\"id\\\" must be a valid GUID\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/invalid", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "invalid" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "get role with missing id", ++ "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\\\" not found\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "00000000-0000-0000-0000-000000000000" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "search roles with admin", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "search roles with booking manager", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_bookingManager}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles?skillsList=dropwizard, nginx,, machine learning , FORce.com &keyword=ops e", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ], ++ "query": [ ++ { ++ "key": "skillsList", ++ "value": "dropwizard, nginx,, machine learning , FORce.com " ++ }, ++ { ++ "key": "keyword", ++ "value": "ops e" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "search roles with connect user", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles?skillsList=dataBase, ,Photoshop&keyword=sale", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ], ++ "query": [ ++ { ++ "key": "skillsList", ++ "value": "dataBase, ,Photoshop" ++ }, ++ { ++ "key": "keyword", ++ "value": "sale" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "search roles with m2m read", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_m2m_read_role}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles?skillsList=DOCKER,.NET&keyword=dev", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ], ++ "query": [ ++ { ++ "key": "skillsList", ++ "value": "DOCKER,.NET" ++ }, ++ { ++ "key": "keyword", ++ "value": "dev" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "search roles with member", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles?keyword=dev", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ], ++ "query": [ ++ { ++ "key": "keyword", ++ "value": "dev" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "search roles 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": "GET", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer invalid token" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ }, ++ { ++ "name": "Update Role", ++ "item": [ ++ { ++ "name": "update role with admin", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer edit\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with booking manager", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_bookingManager}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Angular Developer edit\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-2}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-2}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with m2m update", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_m2m_update_role}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Salesforce Developer edit\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-3}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-3}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 403', function () {\r", ++ " pm.response.to.have.status(403);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 403', function () {\r", ++ " pm.response.to.have.status(403);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role 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": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer invalid_token" ++ } ++ ], ++ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid id", ++ "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(\"\\\"id\\\" must be a valid GUID\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/invalid", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "invalid" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with missing id", ++ "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": "PATCH", ++ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "00000000-0000-0000-0000-000000000000" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with existent name", ++ "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(\"Role: \\\"Angular Developer edit\\\" is already exists.\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Angular Developer edit\"\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 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.name\\\" length must be less than or equal to 50 characters long\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer 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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 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.listOfSkills[0]\\\" length must be less than or equal to 50 characters long\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "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 \"Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking\",\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 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.listOfSkills\\\" must be an array\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "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\":\"Teamworking\",\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 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.rates\\\" must be an array\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 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.rates[0].global\\\" must be a number\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": \"first\",\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 6", ++ "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.rates[0].inCountry\\\" must be a number\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": \"fifty\",\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 7", ++ "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.numberOfMembers\\\" must be a number\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": \"hundred\",\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 8", ++ "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.imageUrl\\\" must be a valid uri\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 9", ++ "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.timeToCandidate\\\" must be less than or equal to 32767\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 99999,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "update role with invalid parameter 10", ++ "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(\"skills: \\\"teamworking\\\" are not valid\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "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 \"Teamworking\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 66,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ }, ++ { ++ "name": "Delete Role", ++ "item": [ ++ { ++ "name": "delete role with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 403', function () {\r", ++ " pm.response.to.have.status(403);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "delete role with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 403', function () {\r", ++ " pm.response.to.have.status(403);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "delete role 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": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer invalid_token" ++ } ++ ], ++ "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 \"teamworking\",\n \"communication\",\n \"problem-solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "delete role with invalid id", ++ "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(\"\\\"id\\\" must be a valid GUID\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/invalid", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "invalid" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "delete role with missing id", ++ "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": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "00000000-0000-0000-0000-000000000000" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "delete role with admin", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 204', function () {\r", ++ " pm.response.to.have.status(204);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "delete role with booking manager", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 204', function () {\r", ++ " pm.response.to.have.status(204);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_bookingManager}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-2}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-2}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "delete role with m2m delete", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 204', function () {\r", ++ " pm.response.to.have.status(204);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_m2m_delete_role}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-3}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-3}}" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ } ++ ] ++ }, + { + "name": "health check", + "item": [ +@@ -22399,7 +25386,227 @@ + ], + "body": { + "mode": "raw", +- "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", ++ "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_by_administrator}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "work-period-payments", ++ "{{workPeriodPaymentId_created_by_administrator}}" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ }, ++ { ++ "name": "Roles", ++ "item": [ ++ { ++ "name": "✔ create role with admin", ++ "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(\"roleId-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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ get role with admin", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ search roles with admin", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ update role with admin", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\n \"name\": \"Dev Ops Engineer edit\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ delete role with admin", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 204', function () {\r", ++ " pm.response.to.have.status(204);\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_administrator}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", + "options": { + "raw": { + "language": "json" +@@ -22407,13 +25614,13 @@ + } + }, + "url": { +- "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_by_administrator}}", ++ "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ +- "work-period-payments", +- "{{workPeriodPaymentId_created_by_administrator}}" ++ "roles", ++ "{{roleId-1}}" + ] + } + }, +@@ -24635,12 +27842,295 @@ + { + "key": "Authorization", + "type": "text", +- "value": "Bearer {{token_member_tester1234}}" ++ "value": "Bearer {{token_member_tester1234}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_for_member}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "work-period-payments", ++ "{{workPeriodPaymentId_created_for_member}}" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ }, ++ { ++ "name": "Roles", ++ "item": [ ++ { ++ "name": "Before Start", ++ "item": [ ++ { ++ "name": "✔ create role with admin", ++ "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(\"roleId-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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ }, ++ { ++ "name": "✘ create role with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 403', function () {\r", ++ " pm.response.to.have.status(403);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ get role with member", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ search roles with member", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles?keyword=Dev", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ], ++ "query": [ ++ { ++ "key": "keyword", ++ "value": "Dev" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✘ update role with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 403', function () {\r", ++ " pm.response.to.have.status(403);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" ++ } ++ ], ++ "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 \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✘ delete role with member", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 403', function () {\r", ++ " pm.response.to.have.status(403);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_member}}" + } + ], + "body": { + "mode": "raw", +- "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", ++ "raw": "", + "options": { + "raw": { + "language": "json" +@@ -24648,13 +28138,13 @@ + } + }, + "url": { +- "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_for_member}}", ++ "raw": "{{URL}}/roles/{{roleId-1}}", + "host": [ + "{{URL}}" + ], + "path": [ +- "work-period-payments", +- "{{workPeriodPaymentId_created_for_member}}" ++ "roles", ++ "{{roleId-1}}" + ] + } + }, +@@ -26894,10 +30384,297 @@ + "response": [] + } + ] ++ }, ++ { ++ "name": "Roles", ++ "item": [ ++ { ++ "name": "Before Start", ++ "item": [ ++ { ++ "name": "✔ create role with admin", ++ "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(\"roleId-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 2\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] ++ }, ++ { ++ "name": "✘ create role with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 403', function () {\r", ++ " pm.response.to.have.status(403);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "POST", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/new", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "new" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ get role with connect user", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✔ search roles with connect user", ++ "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", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "url": { ++ "raw": "{{URL}}/roles?skillsList=Dropwizard, ,NGINX&keyword=Dev", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles" ++ ], ++ "query": [ ++ { ++ "key": "skillsList", ++ "value": "Dropwizard, ,NGINX" ++ }, ++ { ++ "key": "keyword", ++ "value": "Dev" ++ } ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✘ update role with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 403', function () {\r", ++ " pm.response.to.have.status(403);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "PATCH", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "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 \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ }, ++ { ++ "name": "✘ delete role with connect user", ++ "event": [ ++ { ++ "listen": "test", ++ "script": { ++ "exec": [ ++ "pm.test('Status code is 403', function () {\r", ++ " pm.response.to.have.status(403);\r", ++ " const response = pm.response.json()\r", ++ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", ++ "});" ++ ], ++ "type": "text/javascript" ++ } ++ } ++ ], ++ "request": { ++ "method": "DELETE", ++ "header": [ ++ { ++ "key": "Authorization", ++ "type": "text", ++ "value": "Bearer {{token_connectUser}}" ++ } ++ ], ++ "body": { ++ "mode": "raw", ++ "raw": "", ++ "options": { ++ "raw": { ++ "language": "json" ++ } ++ } ++ }, ++ "url": { ++ "raw": "{{URL}}/roles/{{roleId-1}}", ++ "host": [ ++ "{{URL}}" ++ ], ++ "path": [ ++ "roles", ++ "{{roleId-1}}" ++ ] ++ } ++ }, ++ "response": [] ++ } ++ ] + } + ] + } + ] + } + ] +-} ++} +\ No newline at end of file +diff --git a/docs/swagger.yaml b/docs/swagger.yaml +index a0b6064..e5f1ac2 100644 +--- a/docs/swagger.yaml ++++ b/docs/swagger.yaml +@@ -18,6 +18,8 @@ tags: + - name: ResourceBookings + - name: Teams + - name: WorkPeriods ++ - name: WorkPeriodPayments ++ - name: Roles + paths: + /jobs: + post: +@@ -3245,6 +3247,267 @@ paths: + application/json: + schema: + $ref: "#/components/schemas/Error" ++ /roles/new: ++ post: ++ tags: ++ - Roles ++ description: | ++ Create Role. ++ ++ **Authorization** Topcoder m2m token with create scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. ++ security: ++ - bearerAuth: [] ++ requestBody: ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/RoleRequestBody" ++ responses: ++ "200": ++ description: OK ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Role" ++ "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" ++ /roles: ++ get: ++ tags: ++ - Roles ++ description: | ++ Search roles. ++ ++ **Authorization** Topcoder m2m token with read scope is allowed. Topcoder user token with any role is allowed. ++ security: ++ - bearerAuth: [] ++ parameters: ++ - in: query ++ name: skillsList ++ required: false ++ schema: ++ type: string ++ description: comma separated skill names. case-insensitive. ++ - in: query ++ name: keyword ++ required: false ++ schema: ++ type: string ++ description: role name. case-insensitive. partial match allowed ++ responses: ++ "200": ++ description: OK ++ content: ++ application/json: ++ schema: ++ type: array ++ items: ++ $ref: "#/components/schemas/Role" ++ "400": ++ description: Bad request ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "401": ++ description: Not authenticated ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "500": ++ description: Internal Server Error ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ /roles/{id}: ++ get: ++ tags: ++ - Roles ++ description: | ++ Get role by id. ++ ++ **Authorization** Topcoder m2m token with read scope is allowed. Topcoder user token with any role is allowed. ++ security: ++ - bearerAuth: [] ++ parameters: ++ - in: path ++ name: id ++ description: The role id. ++ required: true ++ schema: ++ type: string ++ format: uuid ++ - in: query ++ name: fromDb ++ description: get data from db or not. ++ required: false ++ schema: ++ type: boolean ++ default: false ++ responses: ++ "200": ++ description: OK ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Role" ++ "400": ++ description: Bad request ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Error" ++ "401": ++ description: Not authenticated ++ 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" ++ patch: ++ tags: ++ - Roles ++ description: | ++ Partial Update role. ++ ++ **Authorization** Topcoder m2m token with update scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. ++ security: ++ - bearerAuth: [] ++ parameters: ++ - in: path ++ name: id ++ description: The id of role. ++ required: true ++ schema: ++ type: string ++ format: uuid ++ requestBody: ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/RolePatchRequestBody" ++ responses: ++ "200": ++ description: OK ++ content: ++ application/json: ++ schema: ++ $ref: "#/components/schemas/Role" ++ "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" ++ delete: ++ tags: ++ - Roles ++ description: | ++ Delete the role. ++ ++ **Authorization** Topcoder m2m token with delete scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. ++ security: ++ - bearerAuth: [] ++ parameters: ++ - in: path ++ name: id ++ description: The id of role. ++ required: true ++ schema: ++ type: string ++ format: uuid ++ responses: ++ "204": ++ description: OK ++ "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" + /health: + get: + tags: +@@ -3335,6 +3598,13 @@ components: + type: string + format: uuid + description: "The skill id." ++ roleIds: ++ type: array ++ description: "The roles." ++ items: ++ type: string ++ format: uuid ++ description: "The role id." + status: + type: string + enum: ["sourcing", "in-review", "assigned", "closed", "cancelled"] +@@ -3424,6 +3694,13 @@ components: + type: string + format: uuid + description: "The skill id." ++ roleIds: ++ type: array ++ description: "The roles." ++ items: ++ type: string ++ format: uuid ++ description: "The role id." + isApplicationPageActive: + type: boolean + default: false +@@ -3865,6 +4142,13 @@ components: + type: string + format: uuid + description: "The skill id." ++ roleIds: ++ type: array ++ description: "The roles." ++ items: ++ type: string ++ format: uuid ++ description: "The role id." + isApplicationPageActive: + type: boolean + default: false +@@ -4710,6 +4994,198 @@ components: + type: string + description: "the email of a member" + example: "xxx@xxx.com" ++ Role: ++ required: ++ - id ++ - name ++ - rates ++ - createdAt ++ - createdBy ++ properties: ++ id: ++ type: string ++ format: uuid ++ description: "The role id." ++ name: ++ type: string ++ example: "Dev Ops Engineer" ++ description: "The role name." ++ description: ++ type: string ++ example: "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." ++ description: "The role description" ++ listOfSkills: ++ type: array ++ description: "The array of skill names." ++ items: ++ type: string ++ example: "HTML" ++ description: "The skill name" ++ rates: ++ type: array ++ description: "The rates object array." ++ items: ++ $ref: "#/components/schemas/RoleRates" ++ numberOfMembers: ++ type: number ++ example: 100 ++ description: "The number of members." ++ numberOfMembersAvailable: ++ type: integer ++ example: 100 ++ description: "The number of members available." ++ imageUrl: ++ type: string ++ format: url ++ example: "http://images.topcoder.com/images" ++ description: "The image url of the role." ++ timeToCandidate: ++ type: integer ++ example: 200 ++ description: "The time to candidate." ++ timeToInterview: ++ type: integer ++ example: 300 ++ description: "The time to interview." ++ createdAt: ++ type: string ++ format: date-time ++ description: "The role created date." ++ createdBy: ++ type: string ++ format: uuid ++ description: "The user Id who created the role.(Will get the user info from the token)" ++ updatedAt: ++ type: string ++ format: date-time ++ description: "The role last updated at." ++ updatedBy: ++ type: string ++ format: uuid ++ description: "The user Id who updated the role last time.(Will get the user info from the token)" ++ RoleRequestBody: ++ required: ++ - name ++ - rates ++ properties: ++ name: ++ type: string ++ example: "Dev Ops Engineer" ++ description: "The role name." ++ description: ++ type: string ++ example: "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." ++ description: "The role description" ++ listOfSkills: ++ type: array ++ description: "The array of skill names." ++ items: ++ type: string ++ example: "HTML" ++ description: "The skill name" ++ rates: ++ type: array ++ description: "The rates object array." ++ items: ++ $ref: "#/components/schemas/RoleRates" ++ numberOfMembers: ++ type: number ++ example: 100 ++ description: "The number of members." ++ numberOfMembersAvailable: ++ type: number ++ example: 100 ++ description: "The number of members available." ++ imageUrl: ++ type: string ++ format: url ++ example: "http://images.topcoder.com/images" ++ description: "The image url of the role." ++ timeToCandidate: ++ type: integer ++ example: 200 ++ description: "The time to candidate." ++ timeToInterview: ++ type: integer ++ example: 300 ++ description: "The time to interview." ++ RolePatchRequestBody: ++ properties: ++ name: ++ type: string ++ example: "Dev Ops Engineer" ++ description: "The role name." ++ description: ++ type: string ++ example: "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." ++ description: "The role description" ++ listOfSkills: ++ type: array ++ description: "The array of skill names." ++ items: ++ type: string ++ example: "HTML" ++ description: "The skill name" ++ rates: ++ type: array ++ description: "The rates object array." ++ items: ++ $ref: "#/components/schemas/RoleRates" ++ numberOfMembers: ++ type: number ++ example: 100 ++ description: "The number of members." ++ numberOfMembersAvailable: ++ type: number ++ example: 100 ++ description: "The number of members available." ++ imageUrl: ++ type: string ++ format: url ++ example: "http://images.topcoder.com/images" ++ description: "The image url of the role." ++ timeToCandidate: ++ type: integer ++ example: 200 ++ description: "The time to candidate." ++ timeToInterview: ++ type: integer ++ example: 300 ++ description: "The time to interview." ++ RoleRates: ++ required: ++ - global ++ - inCountry ++ - offShore ++ type: object ++ properties: ++ global: ++ type: integer ++ example: 10 ++ inCountry: ++ type: integer ++ example: 20 ++ offShore: ++ type: integer ++ example: 30 ++ rate30Global: ++ type: integer ++ example: 10 ++ rate30InCountry: ++ type: integer ++ example: 20 ++ rate30OffShore: ++ type: integer ++ example: 30 ++ rate20Global: ++ type: integer ++ example: 10 ++ rate20InCountry: ++ type: integer ++ example: 20 ++ rate20OffShore: ++ type: integer ++ example: 30 + ProjectMember: + type: object + example: +diff --git a/docs/topcoder-bookings.postman_environment.json b/docs/topcoder-bookings.postman_environment.json +index 837b55d..c83fc9a 100644 +--- a/docs/topcoder-bookings.postman_environment.json ++++ b/docs/topcoder-bookings.postman_environment.json +@@ -1,5 +1,5 @@ + { +- "id": "228f4dcc-6914-462e-9b56-3285b643a2f8", ++ "id": "0ce42def-1c70-4c24-8986-914caa57f3c8", + "name": "topcoder-bookings", + "values": [ + { +@@ -312,11 +312,6 @@ + "value": "", + "enabled": true + }, +- { +- "key": "job_id_created_for_member", +- "value": "", +- "enabled": true +- }, + { + "key": "resource_bookings_id_created_for_member", + "value": "", +@@ -327,11 +322,6 @@ + "value": "", + "enabled": true + }, +- { +- "key": "job_id_created_for_connect_manager", +- "value": "", +- "enabled": true +- }, + { + "key": "resource_bookings_id_created_for_connect_manager", + "value": "", +@@ -461,9 +451,49 @@ + "key": "interview_id_created_for_connect_manager", + "value": "", + "enabled": true ++ }, ++ { ++ "key": "token_m2m_create_role", ++ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.f1QP1QTacyDxy7dwzUhBIT8blXCjKn_mnu9Cg59vIc8", ++ "enabled": true ++ }, ++ { ++ "key": "token_m2m_read_role", ++ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJyZWFkOnRhYXMtcm9sZXMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.ZeWS_W2o8YwlvIB_-z0CFFa9zhRjptCk7qNXsPPWxVY", ++ "enabled": true ++ }, ++ { ++ "key": "token_m2m_update_role", ++ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJ1cGRhdGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.0t4k0skZmxAUKuHQrG3ZrO2dgWcDMLD8W1rVluCy7XQ", ++ "enabled": true ++ }, ++ { ++ "key": "token_m2m_delete_role", ++ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJkZWxldGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.NSBbWOk5jCB8nIvLiZwJtR9px5wmUQaQjgpDlMDJ9hk", ++ "enabled": true ++ }, ++ { ++ "key": "token_m2m_all_role", ++ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJhbGw6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.cn0QVTOFnbHJckYqmGcpUBT8wQUxXWwtteWU7uhlDtI", ++ "enabled": true ++ }, ++ { ++ "key": "roleId-1", ++ "value": "", ++ "enabled": true ++ }, ++ { ++ "key": "roleId-2", ++ "value": "", ++ "enabled": true ++ }, ++ { ++ "key": "roleId-3", ++ "value": "", ++ "enabled": true + } + ], + "_postman_variable_scope": "environment", +- "_postman_exported_at": "2021-05-10T05:06:38.661Z", +- "_postman_exported_using": "Postman/8.3.1" ++ "_postman_exported_at": "2021-05-27T01:32:45.726Z", ++ "_postman_exported_using": "Postman/8.5.1" + } +\ No newline at end of file +diff --git a/local/kafka-client/topics.txt b/local/kafka-client/topics.txt +index 8766a1b..760c3a8 100644 +--- a/local/kafka-client/topics.txt ++++ b/local/kafka-client/topics.txt +@@ -3,16 +3,19 @@ taas.jobcandidate.create + taas.resourcebooking.create + taas.workperiod.create + taas.workperiodpayment.create ++taas.role.requested + taas.job.update + taas.jobcandidate.update + taas.resourcebooking.update + taas.workperiod.update + taas.workperiodpayment.update ++taas.role.update + taas.job.delete + taas.jobcandidate.delete + taas.resourcebooking.delete + taas.workperiod.delete + taas.workperiodpayment.delete ++taas.role.delete + taas.interview.requested + taas.interview.update + taas.interview.bulkUpdate +diff --git a/migrations/2021-05-27-1-role-table-create.js b/migrations/2021-05-27-1-role-table-create.js +new file mode 100644 +index 0000000..bce2ae1 +--- /dev/null ++++ b/migrations/2021-05-27-1-role-table-create.js +@@ -0,0 +1,146 @@ ++const config = require('config') ++ ++/* ++ * Create role table ++ */ ++ ++module.exports = { ++ up: async (queryInterface, Sequelize) => { ++ const transaction = await queryInterface.sequelize.transaction() ++ try { ++ await queryInterface.createTable('roles', { ++ id: { ++ type: Sequelize.UUID, ++ primaryKey: true, ++ allowNull: false, ++ defaultValue: Sequelize.UUIDV4 ++ }, ++ name: { ++ type: Sequelize.STRING(50), ++ allowNull: false ++ }, ++ description: { ++ type: Sequelize.STRING(1000) ++ }, ++ listOfSkills: { ++ field: 'list_of_skills', ++ type: Sequelize.ARRAY({ ++ type: Sequelize.STRING(50) ++ }) ++ }, ++ rates: { ++ type: Sequelize.ARRAY({ ++ type: Sequelize.JSONB({ ++ global: { ++ type: Sequelize.SMALLINT, ++ allowNull: false ++ }, ++ inCountry: { ++ field: 'in_country', ++ type: Sequelize.SMALLINT, ++ allowNull: false ++ }, ++ offShore: { ++ field: 'off_shore', ++ type: Sequelize.SMALLINT, ++ allowNull: false ++ }, ++ rate30Global: { ++ field: 'rate30_global', ++ type: Sequelize.SMALLINT ++ }, ++ rate30InCountry: { ++ field: 'rate30_in_country', ++ type: Sequelize.SMALLINT ++ }, ++ rate30OffShore: { ++ field: 'rate30_off_shore', ++ type: Sequelize.SMALLINT ++ }, ++ rate20Global: { ++ field: 'rate20_global', ++ type: Sequelize.SMALLINT ++ }, ++ rate20InCountry: { ++ field: 'rate20_in_country', ++ type: Sequelize.SMALLINT ++ }, ++ rate20OffShore: { ++ field: 'rate20_off_shore', ++ type: Sequelize.SMALLINT ++ } ++ }), ++ allowNull: false ++ }), ++ allowNull: false ++ }, ++ numberOfMembers: { ++ field: 'number_of_members', ++ type: Sequelize.NUMERIC ++ }, ++ numberOfMembersAvailable: { ++ field: 'number_of_members_available', ++ type: Sequelize.SMALLINT ++ }, ++ imageUrl: { ++ field: 'image_url', ++ type: Sequelize.STRING(255) ++ }, ++ timeToCandidate: { ++ field: 'time_to_candidate', ++ type: Sequelize.SMALLINT ++ }, ++ timeToInterview: { ++ field: 'time_to_interview', ++ type: Sequelize.SMALLINT ++ }, ++ 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, ++ transaction ++ }) ++ await queryInterface.addIndex( ++ { ++ tableName: 'roles', ++ schema: config.DB_SCHEMA_NAME ++ }, ++ ['name'], ++ { ++ type: 'UNIQUE', ++ where: { deleted_at: null }, ++ transaction: transaction ++ } ++ ) ++ await transaction.commit() ++ } catch (err) { ++ await transaction.rollback() ++ throw err ++ } ++ }, ++ down: async (queryInterface, Sequelize) => { ++ await queryInterface.dropTable({ ++ tableName: 'roles', ++ schema: config.DB_SCHEMA_NAME ++ }) ++ } ++} +diff --git a/migrations/2021-05-27-2-job-add-roleIds-field.js b/migrations/2021-05-27-2-job-add-roleIds-field.js +new file mode 100644 +index 0000000..a5b9f4b +--- /dev/null ++++ b/migrations/2021-05-27-2-job-add-roleIds-field.js +@@ -0,0 +1,19 @@ ++const config = require('config') ++ ++/* ++ * Add roleIds field to the Job model. ++ */ ++ ++module.exports = { ++ up: async (queryInterface, Sequelize) => { ++ await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'role_ids', ++ { ++ type: Sequelize.ARRAY({ ++ type: Sequelize.UUID ++ }) ++ }) ++ }, ++ down: async (queryInterface, Sequelize) => { ++ await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'role_ids') ++ } ++} +diff --git a/package.json b/package.json +index 0fa24cc..510504f 100644 +--- a/package.json ++++ b/package.json +@@ -15,6 +15,7 @@ + "index:jobs": "node scripts/es/reIndexJobs.js", + "index:job-candidates": "node scripts/es/reIndexJobCandidates.js", + "index:resource-bookings": "node scripts/es/reIndexResourceBookings.js", ++ "index:roles": "node scripts/es/reIndexRoles.js", + "data:export": "node scripts/data/exportData.js", + "data:import": "node scripts/data/importData.js", + "migrate": "npx sequelize db:migrate", +diff --git a/scripts/data/exportData.js b/scripts/data/exportData.js +index 4eee1ad..cb61e58 100644 +--- a/scripts/data/exportData.js ++++ b/scripts/data/exportData.js +@@ -28,7 +28,7 @@ const resourceBookingModelOpts = { + + const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH + const userPrompt = `WARNING: are you sure you want to export all data in the database to a json file with the path ${filePath}? This will overwrite the file.` +-const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts] ++const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts, 'Role'] + + async function exportData () { + await helper.promptUser(userPrompt, async () => { +diff --git a/scripts/data/importData.js b/scripts/data/importData.js +index 2e9c168..a0aeeb6 100644 +--- a/scripts/data/importData.js ++++ b/scripts/data/importData.js +@@ -28,7 +28,7 @@ const resourceBookingModelOpts = { + + const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH + const userPrompt = `WARNING: this would remove existing data. Are you sure you want to import data from a json file with the path ${filePath}?` +-const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts] ++const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts, 'Role'] + + async function importData () { + await helper.promptUser(userPrompt, async () => { +diff --git a/scripts/es/createIndex.js b/scripts/es/createIndex.js +index d2c7294..269cd5a 100644 +--- a/scripts/es/createIndex.js ++++ b/scripts/es/createIndex.js +@@ -8,7 +8,8 @@ const helper = require('../../src/common/helper') + const indices = [ + config.get('esConfig.ES_INDEX_JOB'), + config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), +- config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') ++ config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), ++ config.get('esConfig.ES_INDEX_ROLE') + ] + const userPrompt = `WARNING: Are you sure want to create the following elasticsearch indices: ${indices}?` + +diff --git a/scripts/es/deleteIndex.js b/scripts/es/deleteIndex.js +index 6e30995..724d355 100644 +--- a/scripts/es/deleteIndex.js ++++ b/scripts/es/deleteIndex.js +@@ -8,7 +8,8 @@ const helper = require('../../src/common/helper') + const indices = [ + config.get('esConfig.ES_INDEX_JOB'), + config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), +- config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') ++ config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), ++ config.get('esConfig.ES_INDEX_ROLE') + ] + const userPrompt = `WARNING: this would remove existent data! Are you sure want to delete the following eleasticsearch indices: ${indices}?` + +diff --git a/scripts/es/reIndexAll.js b/scripts/es/reIndexAll.js +index 802695d..0367be1 100644 +--- a/scripts/es/reIndexAll.js ++++ b/scripts/es/reIndexAll.js +@@ -34,6 +34,7 @@ async function indexAll () { + await helper.indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) + await helper.indexBulkDataToES(jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger) + await helper.indexBulkDataToES(resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger) ++ await helper.indexBulkDataToES('Role', config.get('esConfig.ES_INDEX_ROLE'), logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'indexAll' }) +diff --git a/scripts/es/reIndexRoles.js b/scripts/es/reIndexRoles.js +new file mode 100644 +index 0000000..a4507aa +--- /dev/null ++++ b/scripts/es/reIndexRoles.js +@@ -0,0 +1,37 @@ ++/** ++ * Reindex Roles data in Elasticsearch using data from database ++ */ ++const config = require('config') ++const logger = require('../../src/common/logger') ++const helper = require('../../src/common/helper') ++ ++const roleId = helper.getParamFromCliArgs() ++const index = config.get('esConfig.ES_INDEX_ROLE') ++const reIndexAllRolesPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the index ${index}?` ++const reIndexRolePrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the document with id ${roleId} in index ${index}?` ++ ++async function reIndexRoles () { ++ if (roleId === null) { ++ await helper.promptUser(reIndexAllRolesPrompt, async () => { ++ try { ++ await helper.indexBulkDataToES('Role', index, logger) ++ process.exit(0) ++ } catch (err) { ++ logger.logFullError(err, { component: 'reIndexRoles' }) ++ process.exit(1) ++ } ++ }) ++ } else { ++ await helper.promptUser(reIndexRolePrompt, async () => { ++ try { ++ await helper.indexDataToEsById(roleId, 'Role', index, logger) ++ process.exit(0) ++ } catch (err) { ++ logger.logFullError(err, { component: 'reIndexRoles' }) ++ process.exit(1) ++ } ++ }) ++ } ++} ++ ++reIndexRoles() +diff --git a/src/bootstrap.js b/src/bootstrap.js +index 2999f13..896e6c9 100644 +--- a/src/bootstrap.js ++++ b/src/bootstrap.js +@@ -16,7 +16,7 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') + Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') + Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') + Joi.workload = () => Joi.string().valid('full-time', 'fractional') +-Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed') ++Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed') + Joi.title = () => Joi.string().max(128) + Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') + Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) +@@ -26,6 +26,7 @@ Joi.workPeriodPaymentStatus = () => Joi.string().valid('completed', 'cancelled') + // See https://joi.dev/api/?v=17.3.0#string fro details why it's like this. + // In many cases we would like to allow empty string to make it easier to create UI for editing data. + Joi.stringAllowEmpty = () => Joi.string().allow('') ++Joi.smallint = () => Joi.number().min(-32768).max(32767) + + function buildServices (dir) { + const files = fs.readdirSync(dir) +diff --git a/src/common/helper.js b/src/common/helper.js +index 0ce1190..66cf32d 100644 +--- a/src/common/helper.js ++++ b/src/common/helper.js +@@ -2,50 +2,50 @@ + * This file defines helper methods + */ + +-const fs = require('fs'); +-const querystring = require('querystring'); +-const Confirm = require('prompt-confirm'); +-const Bottleneck = require('bottleneck'); +-const AWS = require('aws-sdk'); +-const config = require('config'); +-const HttpStatus = require('http-status-codes'); +-const _ = require('lodash'); +-const request = require('superagent'); +-const elasticsearch = require('@elastic/elasticsearch'); ++const fs = require('fs') ++const querystring = require('querystring') ++const Confirm = require('prompt-confirm') ++const Bottleneck = require('bottleneck') ++const AWS = require('aws-sdk') ++const config = require('config') ++const HttpStatus = require('http-status-codes') ++const _ = require('lodash') ++const request = require('superagent') ++const elasticsearch = require('@elastic/elasticsearch') + const { +- ResponseError: ESResponseError, +-} = require('@elastic/elasticsearch/lib/errors'); +-const errors = require('../common/errors'); +-const logger = require('./logger'); +-const models = require('../models'); +-const eventDispatcher = require('./eventDispatcher'); +-const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper'); +-const moment = require('moment'); ++ ResponseError: ESResponseError ++} = require('@elastic/elasticsearch/lib/errors') ++const errors = require('../common/errors') ++const logger = require('./logger') ++const models = require('../models') ++const eventDispatcher = require('./eventDispatcher') ++const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') ++const moment = require('moment') + + const localLogger = { + debug: (message) => + logger.debug({ + component: 'helper', + context: message.context, +- message: message.message, ++ message: message.message + }), + error: (message) => + logger.error({ + component: 'helper', + context: message.context, +- message: message.message, ++ message: message.message + }), + info: (message) => + logger.info({ + component: 'helper', + context: message.context, +- message: message.message, +- }), +-}; ++ message: message.message ++ }) ++} + +-AWS.config.region = config.esConfig.AWS_REGION; ++AWS.config.region = config.esConfig.AWS_REGION + +-const m2mAuth = require('tc-core-library-js').auth.m2m; ++const m2mAuth = require('tc-core-library-js').auth.m2m + + const m2m = m2mAuth( + _.pick(config, [ +@@ -53,9 +53,9 @@ const m2m = m2mAuth( + 'AUTH0_AUDIENCE', + 'AUTH0_CLIENT_ID', + 'AUTH0_CLIENT_SECRET', +- 'AUTH0_PROXY_SERVER_URL', ++ 'AUTH0_PROXY_SERVER_URL' + ]) +-); ++) + + const m2mForUbahn = m2mAuth({ + AUTH0_AUDIENCE: config.AUTH0_AUDIENCE_UBAHN, +@@ -64,20 +64,20 @@ const m2mForUbahn = m2mAuth({ + 'TOKEN_CACHE_TIME', + 'AUTH0_CLIENT_ID', + 'AUTH0_CLIENT_SECRET', +- 'AUTH0_PROXY_SERVER_URL', +- ]), +-}); ++ 'AUTH0_PROXY_SERVER_URL' ++ ]) ++}) + +-let busApiClient; ++let busApiClient + + /** + * Get bus api client. + * + * @returns {Object} the bus api client + */ +-function getBusApiClient() { ++function getBusApiClient () { + if (busApiClient) { +- return busApiClient; ++ return busApiClient + } + busApiClient = busApi( + _.pick(config, [ +@@ -88,17 +88,17 @@ function getBusApiClient() { + 'AUTH0_CLIENT_SECRET', + 'BUSAPI_URL', + 'KAFKA_ERROR_TOPIC', +- 'AUTH0_PROXY_SERVER_URL', ++ 'AUTH0_PROXY_SERVER_URL' + ]) +- ); +- return busApiClient; ++ ) ++ return busApiClient + } + + // ES Client mapping +-const esClients = {}; ++const esClients = {} + + // The es index property mapping +-const esIndexPropertyMapping = {}; ++const esIndexPropertyMapping = {} + esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { + projectId: { type: 'integer' }, + externalId: { type: 'keyword' }, +@@ -113,11 +113,12 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { + skills: { type: 'keyword' }, + status: { type: 'keyword' }, + isApplicationPageActive: { type: 'boolean' }, ++ roleIds: { type: 'keyword' }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, +- updatedBy: { type: 'keyword' }, +-}; ++ updatedBy: { type: 'keyword' } ++} + esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { + jobId: { type: 'keyword' }, + userId: { type: 'keyword' }, +@@ -150,14 +151,14 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, + updatedBy: { type: 'keyword' }, +- deletedAt: { type: 'date' }, +- }, ++ deletedAt: { type: 'date' } ++ } + }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, +- updatedBy: { type: 'keyword' }, +-}; ++ updatedBy: { type: 'keyword' } ++} + esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { + projectId: { type: 'integer' }, + userId: { type: 'keyword' }, +@@ -195,32 +196,59 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, +- updatedBy: { type: 'keyword' }, +- }, ++ updatedBy: { type: 'keyword' } ++ } + }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, +- updatedBy: { type: 'keyword' }, +- }, ++ updatedBy: { type: 'keyword' } ++ } ++ }, ++ createdAt: { type: 'date' }, ++ createdBy: { type: 'keyword' }, ++ updatedAt: { type: 'date' }, ++ updatedBy: { type: 'keyword' } ++} ++esIndexPropertyMapping[config.get('esConfig.ES_INDEX_ROLE')] = { ++ name: { type: 'keyword' }, ++ description: { type: 'keyword' }, ++ listOfSkills: { type: 'keyword' }, ++ rates: { ++ properties: { ++ global: { type: 'integer' }, ++ inCountry: { type: 'integer' }, ++ offShore: { type: 'integer' }, ++ rate30Global: { type: 'integer' }, ++ rate30InCountry: { type: 'integer' }, ++ rate30OffShore: { type: 'integer' }, ++ rate20Global: { type: 'integer' }, ++ rate20InCountry: { type: 'integer' }, ++ rate20OffShore: { type: 'integer' } ++ } + }, ++ numberOfMembers: { type: 'integer' }, ++ numberOfMembersAvailable: { type: 'integer' }, ++ imageUrl: { type: 'keyword' }, ++ timeToCandidate: { type: 'integer' }, ++ timeToInterview: { type: 'integer' }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, +- updatedBy: { type: 'keyword' }, +-}; ++ updatedBy: { type: 'keyword' } ++} + + /** + * Get the first parameter from cli arguments + */ +-function getParamFromCliArgs() { +- const filteredArgs = process.argv.filter((arg) => !arg.includes('--')); ++function getParamFromCliArgs () { ++ const filteredArgs = process.argv.filter((arg) => !arg.includes('--')) + + if (filteredArgs.length > 2) { +- return filteredArgs[2]; ++ return filteredArgs[2] + } + +- return null; ++ return null + } + + /** +@@ -228,18 +256,18 @@ function getParamFromCliArgs() { + * @param {string} promptQuery the query to ask the user + * @param {function} cb the callback function + */ +-async function promptUser(promptQuery, cb) { ++async function promptUser (promptQuery, cb) { + if (process.argv.includes('--force')) { +- await cb(); +- return; ++ await cb() ++ return + } + +- const prompt = new Confirm(promptQuery); ++ const prompt = new Confirm(promptQuery) + prompt.ask(async (answer) => { + if (answer) { +- await cb(); ++ await cb() + } +- }); ++ }) + } + + /** +@@ -248,23 +276,23 @@ async function promptUser(promptQuery, cb) { + * @param {Object} logger the logger object + * @param {Object} esClient the elasticsearch client (optional, will create if not given) + */ +-async function createIndex(index, logger, esClient = null) { ++async function createIndex (index, logger, esClient = null) { + if (!esClient) { +- esClient = getESClient(); ++ esClient = getESClient() + } + + await esClient.indices.create({ + index, + body: { + mappings: { +- properties: esIndexPropertyMapping[index], +- }, +- }, +- }); ++ properties: esIndexPropertyMapping[index] ++ } ++ } ++ }) + logger.info({ + component: 'createIndex', +- message: `ES Index ${index} creation succeeded!`, +- }); ++ message: `ES Index ${index} creation succeeded!` ++ }) + } + + /** +@@ -273,45 +301,45 @@ async function createIndex(index, logger, esClient = null) { + * @param {Object} logger the logger object + * @param {Object} esClient the elasticsearch client (optional, will create if not given) + */ +-async function deleteIndex(index, logger, esClient = null) { ++async function deleteIndex (index, logger, esClient = null) { + if (!esClient) { +- esClient = getESClient(); ++ esClient = getESClient() + } + +- await esClient.indices.delete({ index }); ++ await esClient.indices.delete({ index }) + logger.info({ + component: 'deleteIndex', +- message: `ES Index ${index} deletion succeeded!`, +- }); ++ message: `ES Index ${index} deletion succeeded!` ++ }) + } + + /** + * Split data into bulks + * @param {Array} data the array of data to split + */ +-function getBulksFromDocuments(data) { +- const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6; +- const bulks = []; +- let documentIndex = 0; +- let currentBulkSize = 0; +- let currentBulk = []; ++function getBulksFromDocuments (data) { ++ const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6 ++ const bulks = [] ++ let documentIndex = 0 ++ let currentBulkSize = 0 ++ let currentBulk = [] + + while (true) { + // break loop when parsed all documents + if (documentIndex >= data.length) { +- bulks.push(currentBulk); +- break; ++ bulks.push(currentBulk) ++ break + } + + // check if current document size is greater than the max bulk size, if so, throw error + const currentDocumentSize = Buffer.byteLength( + JSON.stringify(data[documentIndex]), + 'utf-8' +- ); ++ ) + if (maxBytes < currentDocumentSize) { + throw new Error( + `Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.` +- ); ++ ) + } + + if ( +@@ -320,17 +348,17 @@ function getBulksFromDocuments(data) { + ) { + // if adding the current document goes over the max bulk size OR goes over max number of docs + // then push the current bulk to bulks array and reset the current bulk +- bulks.push(currentBulk); +- currentBulk = []; +- currentBulkSize = 0; ++ bulks.push(currentBulk) ++ currentBulk = [] ++ currentBulkSize = 0 + } else { + // otherwise, add document to current bulk +- currentBulk.push(data[documentIndex]); +- currentBulkSize += currentDocumentSize; +- documentIndex++; ++ currentBulk.push(data[documentIndex]) ++ currentBulkSize += currentDocumentSize ++ documentIndex++ + } + } +- return bulks; ++ return bulks + } + + /** +@@ -339,57 +367,57 @@ function getBulksFromDocuments(data) { + * @param {Object} indexName the index name + * @param {Object} logger the logger object + */ +-async function indexBulkDataToES(modelOpts, indexName, logger) { +- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; +- const include = _.get(modelOpts, 'include', []); ++async function indexBulkDataToES (modelOpts, indexName, logger) { ++ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName ++ const include = _.get(modelOpts, 'include', []) + + logger.info({ + component: 'indexBulkDataToES', +- message: `Reindexing of ${modelName}s started!`, +- }); ++ message: `Reindexing of ${modelName}s started!` ++ }) + +- const esClient = getESClient(); ++ const esClient = getESClient() + + // clear index +- const indexExistsRes = await esClient.indices.exists({ index: indexName }); ++ const indexExistsRes = await esClient.indices.exists({ index: indexName }) + if (indexExistsRes.statusCode !== 404) { +- await deleteIndex(indexName, logger, esClient); ++ await deleteIndex(indexName, logger, esClient) + } +- await createIndex(indexName, logger, esClient); ++ await createIndex(indexName, logger, esClient) + + // get data from db + logger.info({ + component: 'indexBulkDataToES', +- message: 'Getting data from database', +- }); +- const model = models[modelName]; +- const data = await model.findAll({ include }); +- const rawObjects = _.map(data, (r) => r.toJSON()); ++ message: 'Getting data from database' ++ }) ++ const model = models[modelName] ++ const data = await model.findAll({ include }) ++ const rawObjects = _.map(data, (r) => r.toJSON()) + if (_.isEmpty(rawObjects)) { + logger.info({ + component: 'indexBulkDataToES', +- message: `No data in database for ${modelName}`, +- }); +- return; ++ message: `No data in database for ${modelName}` ++ }) ++ return + } +- const bulks = getBulksFromDocuments(rawObjects); ++ const bulks = getBulksFromDocuments(rawObjects) + +- const startTime = Date.now(); +- let doneCount = 0; ++ const startTime = Date.now() ++ let doneCount = 0 + for (const bulk of bulks) { + // send bulk to esclient + const body = bulk.flatMap((doc) => [ + { index: { _index: indexName, _id: doc.id } }, +- doc, +- ]); +- await esClient.bulk({ refresh: true, body }); +- doneCount += bulk.length; ++ doc ++ ]) ++ await esClient.bulk({ refresh: true, body }) ++ doneCount += bulk.length + + // log metrics +- const timeSpent = Date.now() - startTime; +- const avgTimePerDocument = timeSpent / doneCount; +- const estimatedLength = avgTimePerDocument * data.length; +- const timeLeft = startTime + estimatedLength - Date.now(); ++ const timeSpent = Date.now() - startTime ++ const avgTimePerDocument = timeSpent / doneCount ++ const estimatedLength = avgTimePerDocument * data.length ++ const timeLeft = startTime + estimatedLength - Date.now() + logger.info({ + component: 'indexBulkDataToES', + message: `Processed ${doneCount} of ${ +@@ -398,8 +426,8 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { + avgTimePerDocument + )}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime( + timeLeft +- )}`, +- }); ++ )}` ++ }) + } + } + +@@ -410,36 +438,36 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { + * @param {string} id the job id + * @param {Object} logger the logger object + */ +-async function indexDataToEsById(id, modelOpts, indexName, logger) { +- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; +- const include = _.get(modelOpts, 'include', []); ++async function indexDataToEsById (id, modelOpts, indexName, logger) { ++ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName ++ const include = _.get(modelOpts, 'include', []) + + logger.info({ + component: 'indexDataToEsById', +- message: `Reindexing of ${modelName} with id ${id} started!`, +- }); +- const esClient = getESClient(); ++ message: `Reindexing of ${modelName} with id ${id} started!` ++ }) ++ const esClient = getESClient() + + logger.info({ + component: 'indexDataToEsById', +- message: 'Getting data from database', +- }); +- const model = models[modelName]; ++ message: 'Getting data from database' ++ }) ++ const model = models[modelName] + +- const data = await model.findById(id, include); ++ const data = await model.findById(id, include) + logger.info({ + component: 'indexDataToEsById', +- message: 'Indexing data into Elasticsearch', +- }); ++ message: 'Indexing data into Elasticsearch' ++ }) + await esClient.index({ + index: indexName, + id: id, +- body: data.dataValues, +- }); ++ body: data.dataValues ++ }) + logger.info({ + component: 'indexDataToEsById', +- message: 'Indexing complete!', +- }); ++ message: 'Indexing complete!' ++ }) + } + + /** +@@ -448,68 +476,68 @@ async function indexDataToEsById(id, modelOpts, indexName, logger) { + * @param {Array} dataModels the data models to import + * @param {Object} logger the logger object + */ +-async function importData(pathToFile, dataModels, logger) { ++async function importData (pathToFile, dataModels, logger) { + // check if file exists + if (!fs.existsSync(pathToFile)) { +- throw new Error(`File with path ${pathToFile} does not exist`); ++ throw new Error(`File with path ${pathToFile} does not exist`) + } + + // clear database +- logger.info({ component: 'importData', message: 'Clearing database...' }); +- await models.sequelize.sync({ force: true }); ++ logger.info({ component: 'importData', message: 'Clearing database...' }) ++ await models.sequelize.sync({ force: true }) + +- let transaction = null; +- let currentModelName = null; ++ let transaction = null ++ let currentModelName = null + try { + // Start a transaction +- transaction = await models.sequelize.transaction(); +- const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()); ++ transaction = await models.sequelize.transaction() ++ const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()) + + for (let index = 0; index < dataModels.length; index += 1) { +- const modelOpts = dataModels[index]; +- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; +- const include = _.get(modelOpts, 'include', []); ++ const modelOpts = dataModels[index] ++ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName ++ const include = _.get(modelOpts, 'include', []) + +- currentModelName = modelName; +- const model = models[modelName]; +- const modelRecords = jsonData[modelName]; ++ currentModelName = modelName ++ const model = models[modelName] ++ const modelRecords = jsonData[modelName] + + if (modelRecords && modelRecords.length > 0) { + logger.info({ + component: 'importData', +- message: `Importing data for model: ${modelName}`, +- }); ++ message: `Importing data for model: ${modelName}` ++ }) + +- await model.bulkCreate(modelRecords, { include, transaction }); ++ await model.bulkCreate(modelRecords, { include, transaction }) + logger.info({ + component: 'importData', +- message: `Records imported for model: ${modelName} = ${modelRecords.length}`, +- }); ++ message: `Records imported for model: ${modelName} = ${modelRecords.length}` ++ }) + } else { + logger.info({ + component: 'importData', +- message: `No records to import for model: ${modelName}`, +- }); ++ message: `No records to import for model: ${modelName}` ++ }) + } + } + // commit transaction only if all things went ok + logger.info({ + component: 'importData', +- message: 'committing transaction to database...', +- }); +- await transaction.commit(); ++ message: 'committing transaction to database...' ++ }) ++ await transaction.commit() + } catch (error) { + logger.error({ + component: 'importData', +- message: `Error while writing data of model: ${currentModelName}`, +- }); ++ message: `Error while writing data of model: ${currentModelName}` ++ }) + // rollback all insert operations + if (transaction) { + logger.info({ + component: 'importData', +- message: 'rollback database transaction...', +- }); +- transaction.rollback(); ++ message: 'rollback database transaction...' ++ }) ++ transaction.rollback() + } + if (error.name && error.errors && error.fields) { + // For sequelize validation errors, we throw only fields with data that helps in debugging error, +@@ -519,11 +547,11 @@ async function importData(pathToFile, dataModels, logger) { + modelName: currentModelName, + name: error.name, + errors: error.errors, +- fields: error.fields, ++ fields: error.fields + }) +- ); ++ ) + } else { +- throw error; ++ throw error + } + } + +@@ -533,10 +561,10 @@ async function importData(pathToFile, dataModels, logger) { + include: [ + { + model: models.Interview, +- as: 'interviews', +- }, +- ], +- }; ++ as: 'interviews' ++ } ++ ] ++ } + const resourceBookingModelOpts = { + modelName: 'ResourceBooking', + include: [ +@@ -546,23 +574,24 @@ async function importData(pathToFile, dataModels, logger) { + include: [ + { + model: models.WorkPeriodPayment, +- as: 'payments', +- }, +- ], +- }, +- ], +- }; +- await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger); ++ as: 'payments' ++ } ++ ] ++ } ++ ] ++ } ++ await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) + await indexBulkDataToES( + jobCandidateModelOpts, + config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), + logger +- ); ++ ) + await indexBulkDataToES( + resourceBookingModelOpts, + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + logger +- ); ++ ) ++ await indexBulkDataToES('Role', config.get('esConfig.ES_INDEX_ROLE'), logger) + } + + /** +@@ -571,74 +600,74 @@ async function importData(pathToFile, dataModels, logger) { + * @param {Array} dataModels the data models to export + * @param {Object} logger the logger object + */ +-async function exportData(pathToFile, dataModels, logger) { ++async function exportData (pathToFile, dataModels, logger) { + logger.info({ + component: 'exportData', +- message: `Start Saving data to file with path ${pathToFile}....`, +- }); ++ message: `Start Saving data to file with path ${pathToFile}....` ++ }) + +- const allModelsRecords = {}; ++ const allModelsRecords = {} + for (let index = 0; index < dataModels.length; index += 1) { +- const modelOpts = dataModels[index]; +- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; +- const include = _.get(modelOpts, 'include', []); +- const modelRecords = await models[modelName].findAll({ include }); +- const rawRecords = _.map(modelRecords, (r) => r.toJSON()); +- allModelsRecords[modelName] = rawRecords; ++ const modelOpts = dataModels[index] ++ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName ++ const include = _.get(modelOpts, 'include', []) ++ const modelRecords = await models[modelName].findAll({ include }) ++ const rawRecords = _.map(modelRecords, (r) => r.toJSON()) ++ allModelsRecords[modelName] = rawRecords + logger.info({ + component: 'exportData', +- message: `Records loaded for model: ${modelName} = ${rawRecords.length}`, +- }); ++ message: `Records loaded for model: ${modelName} = ${rawRecords.length}` ++ }) + } + +- fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)); ++ fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)) + logger.info({ + component: 'exportData', +- message: 'End Saving data to file....', +- }); ++ message: 'End Saving data to file....' ++ }) + } + + /** + * Format a time in milliseconds into a human readable format + * @param {Date} milliseconds the number of milliseconds + */ +-function formatTime(millisec) { +- const ms = Math.floor(millisec % 1000); +- const secs = Math.floor((millisec / 1000) % 60); +- const mins = Math.floor((millisec / (1000 * 60)) % 60); +- const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24); +- const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7); +- const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4); +- const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12); +- const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)); +- +- let formattedTime = '0 milliseconds'; ++function formatTime (millisec) { ++ const ms = Math.floor(millisec % 1000) ++ const secs = Math.floor((millisec / 1000) % 60) ++ const mins = Math.floor((millisec / (1000 * 60)) % 60) ++ const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24) ++ const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7) ++ const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4) ++ const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12) ++ const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)) ++ ++ let formattedTime = '0 milliseconds' + if (ms > 0) { +- formattedTime = `${ms} milliseconds`; ++ formattedTime = `${ms} milliseconds` + } + if (secs > 0) { +- formattedTime = `${secs} seconds ${formattedTime}`; ++ formattedTime = `${secs} seconds ${formattedTime}` + } + if (mins > 0) { +- formattedTime = `${mins} minutes ${formattedTime}`; ++ formattedTime = `${mins} minutes ${formattedTime}` + } + if (hrs > 0) { +- formattedTime = `${hrs} hours ${formattedTime}`; ++ formattedTime = `${hrs} hours ${formattedTime}` + } + if (days > 0) { +- formattedTime = `${days} days ${formattedTime}`; ++ formattedTime = `${days} days ${formattedTime}` + } + if (weeks > 0) { +- formattedTime = `${weeks} weeks ${formattedTime}`; ++ formattedTime = `${weeks} weeks ${formattedTime}` + } + if (mnths > 0) { +- formattedTime = `${mnths} months ${formattedTime}`; ++ formattedTime = `${mnths} months ${formattedTime}` + } + if (yrs > 0) { +- formattedTime = `${yrs} years ${formattedTime}`; ++ formattedTime = `${yrs} years ${formattedTime}` + } + +- return formattedTime.trim(); ++ return formattedTime.trim() + } + + /** +@@ -647,30 +676,30 @@ function formatTime(millisec) { + * @param {Array} source the array in which to search for the term + * @param {Array | String} term the term to search + */ +-function checkIfExists(source, term) { +- let terms; ++function checkIfExists (source, term) { ++ let terms + + if (!_.isArray(source)) { +- throw new Error('Source argument should be an array'); ++ throw new Error('Source argument should be an array') + } + +- source = source.map((s) => s.toLowerCase()); ++ source = source.map((s) => s.toLowerCase()) + + if (_.isString(term)) { +- terms = term.toLowerCase().split(' '); ++ terms = term.toLowerCase().split(' ') + } else if (_.isArray(term)) { +- terms = term.map((t) => t.toLowerCase()); ++ terms = term.map((t) => t.toLowerCase()) + } else { +- throw new Error('Term argument should be either a string or an array'); ++ throw new Error('Term argument should be either a string or an array') + } + + for (let i = 0; i < terms.length; i++) { + if (source.includes(terms[i])) { +- return true; ++ return true + } + } + +- return false; ++ return false + } + + /** +@@ -678,10 +707,10 @@ function checkIfExists(source, term) { + * @param {Function} fn the async function + * @returns {Function} the wrapped function + */ +-function wrapExpress(fn) { ++function wrapExpress (fn) { + return function (req, res, next) { +- fn(req, res, next).catch(next); +- }; ++ fn(req, res, next).catch(next) ++ } + } + + /** +@@ -689,20 +718,20 @@ function wrapExpress(fn) { + * @param obj the object (controller exports) + * @returns {Object|Array} the wrapped object + */ +-function autoWrapExpress(obj) { ++function autoWrapExpress (obj) { + if (_.isArray(obj)) { +- return obj.map(autoWrapExpress); ++ return obj.map(autoWrapExpress) + } + if (_.isFunction(obj)) { + if (obj.constructor.name === 'AsyncFunction') { +- return wrapExpress(obj); ++ return wrapExpress(obj) + } +- return obj; ++ return obj + } + _.each(obj, (value, key) => { +- obj[key] = autoWrapExpress(value); +- }); +- return obj; ++ obj[key] = autoWrapExpress(value) ++ }) ++ return obj + } + + /** +@@ -711,11 +740,11 @@ function autoWrapExpress(obj) { + * @param {Number} page the page number + * @returns {String} link for the page + */ +-function getPageLink(req, page) { +- const q = _.assignIn({}, req.query, { page }); ++function getPageLink (req, page) { ++ const q = _.assignIn({}, req.query, { page }) + return `${req.protocol}://${req.get('Host')}${req.baseUrl}${ + req.path +- }?${querystring.stringify(q)}`; ++ }?${querystring.stringify(q)}` + } + + /** +@@ -724,31 +753,31 @@ function getPageLink(req, page) { + * @param {Object} res the HTTP response + * @param {Object} result the operation result + */ +-function setResHeaders(req, res, result) { +- const totalPages = Math.ceil(result.total / result.perPage); ++function setResHeaders (req, res, result) { ++ const totalPages = Math.ceil(result.total / result.perPage) + if (result.page > 1) { +- res.set('X-Prev-Page', result.page - 1); ++ res.set('X-Prev-Page', result.page - 1) + } + if (result.page < totalPages) { +- res.set('X-Next-Page', result.page + 1); ++ res.set('X-Next-Page', result.page + 1) + } +- res.set('X-Page', result.page); +- res.set('X-Per-Page', result.perPage); +- res.set('X-Total', result.total); +- res.set('X-Total-Pages', totalPages); ++ res.set('X-Page', result.page) ++ res.set('X-Per-Page', result.perPage) ++ res.set('X-Total', result.total) ++ res.set('X-Total-Pages', totalPages) + // set Link header + if (totalPages > 0) { + let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink( + req, + totalPages +- )}>; rel="last"`; ++ )}>; rel="last"` + if (result.page > 1) { +- link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"`; ++ link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"` + } + if (result.page < totalPages) { +- link += `, <${getPageLink(req, result.page + 1)}>; rel="next"`; ++ link += `, <${getPageLink(req, result.page + 1)}>; rel="next"` + } +- res.set('Link', link); ++ res.set('Link', link) + } + } + +@@ -756,30 +785,30 @@ function setResHeaders(req, res, result) { + * Get ES Client + * @return {Object} Elastic Host Client Instance + */ +-function getESClient() { ++function getESClient () { + if (esClients.client) { +- return esClients.client; ++ return esClients.client + } + +- const host = config.esConfig.HOST; +- const cloudId = config.esConfig.ELASTICCLOUD.id; ++ const host = config.esConfig.HOST ++ const cloudId = config.esConfig.ELASTICCLOUD.id + if (cloudId) { + // Elastic Cloud configuration + esClients.client = new elasticsearch.Client({ + cloud: { +- id: cloudId, ++ id: cloudId + }, + auth: { + username: config.esConfig.ELASTICCLOUD.username, +- password: config.esConfig.ELASTICCLOUD.password, +- }, +- }); ++ password: config.esConfig.ELASTICCLOUD.password ++ } ++ }) + } else { + esClients.client = new elasticsearch.Client({ +- node: host, +- }); ++ node: host ++ }) + } +- return esClients.client; ++ return esClients.client + } + + /* +@@ -790,8 +819,8 @@ const getM2MToken = async () => { + return await m2m.getMachineToken( + config.AUTH0_CLIENT_ID, + config.AUTH0_CLIENT_SECRET +- ); +-}; ++ ) ++} + + /* + * Function to get M2M token for U-Bahn +@@ -801,8 +830,8 @@ const getM2MUbahnToken = async () => { + return await m2mForUbahn.getMachineToken( + config.AUTH0_CLIENT_ID, + config.AUTH0_CLIENT_SECRET +- ); +-}; ++ ) ++} + + /** + * Function to encode query string +@@ -810,17 +839,17 @@ const getM2MUbahnToken = async () => { + * @param {String} nesting the nesting string + * @returns {String} query string + */ +-function encodeQueryString(queryObj, nesting = '') { ++function encodeQueryString (queryObj, nesting = '') { + const pairs = Object.entries(queryObj).map(([key, val]) => { + // Handle the nested, recursive case, where the value to encode is an object itself + if (typeof val === 'object') { +- return encodeQueryString(val, nesting + `${key}.`); ++ return encodeQueryString(val, nesting + `${key}.`) + } else { + // Handle base case, where the value to encode is simply a string. +- return [nesting + key, val].map(querystring.escape).join('='); ++ return [nesting + key, val].map(querystring.escape).join('=') + } +- }); +- return pairs.join('&'); ++ }) ++ return pairs.join('&') + } + + /** +@@ -828,31 +857,31 @@ function encodeQueryString(queryObj, nesting = '') { + * @param {Integer} externalId the legacy user id + * @returns {Array} the users found + */ +-async function listUsersByExternalId(externalId) { ++async function listUsersByExternalId (externalId) { + // return empty list if externalId is null or undefined + if (!!externalId !== true) { +- return []; ++ return [] + } + +- const token = await getM2MUbahnToken(); ++ const token = await getM2MUbahnToken() + const q = { + enrich: true, + externalProfile: { + organizationId: config.ORG_ID, +- externalId, +- }, +- }; +- const url = `${config.TC_API}/users?${encodeQueryString(q)}`; ++ externalId ++ } ++ } ++ const url = `${config.TC_API}/users?${encodeQueryString(q)}` + const res = await request + .get(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'listUserByExternalId', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return res.body; ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return res.body + } + + /** +@@ -860,14 +889,14 @@ async function listUsersByExternalId(externalId) { + * @param {Integer} externalId the legacy user id + * @returns {Object} the user + */ +-async function getUserByExternalId(externalId) { +- const users = await listUsersByExternalId(externalId); ++async function getUserByExternalId (externalId) { ++ const users = await listUsersByExternalId(externalId) + if (_.isEmpty(users)) { + throw new errors.NotFoundError( + `externalId: ${externalId} "user" not found` +- ); ++ ) + } +- return users[0]; ++ return users[0] + } + + /** +@@ -876,24 +905,24 @@ async function getUserByExternalId(externalId) { + * @params {Object} payload the payload + * @params {Object} options the extra options to control the function + */ +-async function postEvent(topic, payload, options = {}) { ++async function postEvent (topic, payload, options = {}) { + logger.debug({ + component: 'helper', + context: 'postEvent', + message: `Posting event to Kafka topic ${topic}, ${JSON.stringify( + payload +- )}`, +- }); +- const client = getBusApiClient(); ++ )}` ++ }) ++ const client = getBusApiClient() + const message = { + topic, + originator: config.KAFKA_MESSAGE_ORIGINATOR, + timestamp: new Date().toISOString(), + 'mime-type': 'application/json', +- payload, +- }; +- await client.postEvent(message); +- await eventDispatcher.handleEvent(topic, { value: payload, options }); ++ payload ++ } ++ await client.postEvent(message) ++ await eventDispatcher.handleEvent(topic, { value: payload, options }) + } + + /** +@@ -902,11 +931,11 @@ async function postEvent(topic, payload, options = {}) { + * @param {Object} err the err + * @returns {Boolean} the result + */ +-function isDocumentMissingException(err) { ++function isDocumentMissingException (err) { + if (err.statusCode === 404 && err instanceof ESResponseError) { +- return true; ++ return true + } +- return false; ++ return false + } + + /** +@@ -915,34 +944,34 @@ function isDocumentMissingException(err) { + * @param {Object} criteria the search criteria + * @returns the request result + */ +-async function getProjects(currentUser, criteria = {}) { +- let token; ++async function getProjects (currentUser, criteria = {}) { ++ let token + if (currentUser.hasManagePermission || currentUser.isMachine) { +- const m2mToken = await getM2MToken(); +- token = `Bearer ${m2mToken}`; ++ const m2mToken = await getM2MToken() ++ token = `Bearer ${m2mToken}` + } else { +- token = currentUser.jwtToken; ++ token = currentUser.jwtToken + } +- const url = `${config.TC_API}/projects?type=talent-as-a-service`; ++ const url = `${config.TC_API}/projects?type=talent-as-a-service` + const res = await request + .get(url) + .query(criteria) + .set('Authorization', token) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getProjects', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) + const result = _.map(res.body, (item) => { +- return _.pick(item, ['id', 'name', 'invites', 'members']); +- }); ++ return _.pick(item, ['id', 'name', 'invites', 'members']) ++ }) + return { + total: Number(_.get(res.headers, 'x-total')), + page: Number(_.get(res.headers, 'x-page')), + perPage: Number(_.get(res.headers, 'x-per-page')), +- result, +- }; ++ result ++ } + } + + /** +@@ -951,24 +980,24 @@ async function getProjects(currentUser, criteria = {}) { + * @param {String} userId the legacy user id + * @returns {Object} the user + */ +-async function getTopcoderUserById(userId) { +- const token = await getM2MToken(); ++async function getTopcoderUserById (userId) { ++ const token = await getM2MToken() + const res = await request + .get(config.TOPCODER_USERS_API) + .query({ filter: `id=${userId}` }) + .set('Authorization', `Bearer ${token}`) +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getTopcoderUserById', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- const user = _.get(res.body, 'result.content[0]'); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ const user = _.get(res.body, 'result.content[0]') + if (!user) { + throw new errors.NotFoundError( + `userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}` +- ); ++ ) + } +- return user; ++ return user + } + + /** +@@ -976,31 +1005,31 @@ async function getTopcoderUserById(userId) { + * @param {String} userId the user id + * @returns the request result + */ +-async function getUserById(userId, enrich) { +- const token = await getM2MUbahnToken(); ++async function getUserById (userId, enrich) { ++ const token = await getM2MUbahnToken() + const res = await request + .get(`${config.TC_API}/users/${userId}` + (enrich ? '?enrich=true' : '')) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getUserById', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) + +- const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']); ++ const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']) + + if (enrich) { + user.skills = (res.body.skills || []).map((skillObj) => + _.pick(skillObj.skill, ['id', 'name']) +- ); +- const attributes = _.get(res, 'body.attributes', []); ++ ) ++ const attributes = _.get(res, 'body.attributes', []) + user.attributes = _.map(attributes, (attr) => + _.pick(attr, ['id', 'value', 'attribute.id', 'attribute.name']) +- ); ++ ) + } + +- return user; ++ return user + } + + /** +@@ -1008,19 +1037,19 @@ async function getUserById(userId, enrich) { + * @param {Object} data the user data + * @returns the request result + */ +-async function createUbahnUser({ handle, firstName, lastName }) { +- const token = await getM2MUbahnToken(); ++async function createUbahnUser ({ handle, firstName, lastName }) { ++ const token = await getM2MUbahnToken() + const res = await request + .post(`${config.TC_API}/users`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') +- .send({ handle, firstName, lastName }); ++ .send({ handle, firstName, lastName }) + localLogger.debug({ + context: 'createUbahnUser', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return _.pick(res.body, ['id']); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return _.pick(res.body, ['id']) + } + + /** +@@ -1028,21 +1057,21 @@ async function createUbahnUser({ handle, firstName, lastName }) { + * @param {String} userId the user id(with uuid format) + * @param {Object} data the profile data + */ +-async function createUserExternalProfile( ++async function createUserExternalProfile ( + userId, + { organizationId, externalId } + ) { +- const token = await getM2MUbahnToken(); ++ const token = await getM2MUbahnToken() + const res = await request + .post(`${config.TC_API}/users/${userId}/externalProfiles`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') +- .send({ organizationId, externalId: String(externalId) }); ++ .send({ organizationId, externalId: String(externalId) }) + localLogger.debug({ + context: 'createUserExternalProfile', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) + } + + /** +@@ -1050,23 +1079,23 @@ async function createUserExternalProfile( + * @param {Array} handles the handle array + * @returns the request result + */ +-async function getMembers(handles) { +- const token = await getM2MToken(); ++async function getMembers (handles) { ++ const token = await getM2MToken() + const handlesStr = _.map(handles, (handle) => { +- return '%22' + handle.toLowerCase() + '%22'; +- }).join(','); +- const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]`; ++ return '%22' + handle.toLowerCase() + '%22' ++ }).join(',') ++ const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]` + + const res = await request + .get(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getMembers', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return res.body; ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return res.body + } + + /** +@@ -1075,36 +1104,36 @@ async function getMembers(handles) { + * @param {Number} id project id + * @returns the request result + */ +-async function getProjectById(currentUser, id) { +- let token; ++async function getProjectById (currentUser, id) { ++ let token + if (currentUser.hasManagePermission || currentUser.isMachine) { +- const m2mToken = await getM2MToken(); +- token = `Bearer ${m2mToken}`; ++ const m2mToken = await getM2MToken() ++ token = `Bearer ${m2mToken}` + } else { +- token = currentUser.jwtToken; ++ token = currentUser.jwtToken + } +- const url = `${config.TC_API}/projects/${id}`; ++ const url = `${config.TC_API}/projects/${id}` + try { + const res = await request + .get(url) + .set('Authorization', token) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getProjectById', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return _.pick(res.body, ['id', 'name', 'invites', 'members']); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return _.pick(res.body, ['id', 'name', 'invites', 'members']) + } catch (err) { + if (err.status === HttpStatus.FORBIDDEN) { + throw new errors.ForbiddenError( + `You are not allowed to access the project with id ${id}` +- ); ++ ) + } + if (err.status === HttpStatus.NOT_FOUND) { +- throw new errors.NotFoundError(`id: ${id} project not found`); ++ throw new errors.NotFoundError(`id: ${id} project not found`) + } +- throw err; ++ throw err + } + } + +@@ -1115,33 +1144,33 @@ async function getProjectById(currentUser, id) { + * @param {Object} criteria the search criteria + * @returns the request result + */ +-async function getTopcoderSkills(criteria) { +- const token = await getM2MUbahnToken(); ++async function getTopcoderSkills (criteria) { ++ const token = await getM2MUbahnToken() + try { + const res = await request + .get(`${config.TC_API}/skills`) + .query({ + skillProviderId: config.TOPCODER_SKILL_PROVIDER_ID, +- ...criteria, ++ ...criteria + }) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getTopcoderSkills', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) + return { + total: Number(_.get(res.headers, 'x-total')), + page: Number(_.get(res.headers, 'x-page')), + perPage: Number(_.get(res.headers, 'x-per-page')), +- result: res.body, +- }; ++ result: res.body ++ } + } catch (err) { + if (err.status === HttpStatus.BAD_REQUEST) { +- throw new errors.BadRequestError(err.response.body.message); ++ throw new errors.BadRequestError(err.response.body.message) + } +- throw err; ++ throw err + } + } + +@@ -1150,18 +1179,18 @@ async function getTopcoderSkills(criteria) { + * @param {String} skillId the skill Id + * @returns the request result + */ +-async function getSkillById(skillId) { +- const token = await getM2MUbahnToken(); ++async function getSkillById (skillId) { ++ const token = await getM2MUbahnToken() + const res = await request + .get(`${config.TC_API}/skills/${skillId}`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getSkillById', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return _.pick(res.body, ['id', 'name']); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return _.pick(res.body, ['id', 'name']) + } + + /** +@@ -1174,22 +1203,22 @@ async function getSkillById(skillId) { + * @params {Object} currentUser the user who perform this operation + * @returns {String} the ubahn user id + */ +-async function ensureUbahnUserId(currentUser) { ++async function ensureUbahnUserId (currentUser) { + try { +- return (await getUserByExternalId(currentUser.userId)).id; ++ return (await getUserByExternalId(currentUser.userId)).id + } catch (err) { + if (!(err instanceof errors.NotFoundError)) { +- throw err; ++ throw err + } +- const topcoderUser = await getTopcoderUserById(currentUser.userId); ++ const topcoderUser = await getTopcoderUserById(currentUser.userId) + const user = await createUbahnUser( + _.pick(topcoderUser, ['handle', 'firstName', 'lastName']) +- ); ++ ) + await createUserExternalProfile(user.id, { + organizationId: config.ORG_ID, +- externalId: currentUser.userId, +- }); +- return user.id; ++ externalId: currentUser.userId ++ }) ++ return user.id + } + } + +@@ -1199,8 +1228,8 @@ async function ensureUbahnUserId(currentUser) { + * @param {String} jobId the job id + * @returns {Object} the job data + */ +-async function ensureJobById(jobId) { +- return models.Job.findById(jobId); ++async function ensureJobById (jobId) { ++ return models.Job.findById(jobId) + } + + /** +@@ -1209,8 +1238,8 @@ async function ensureJobById(jobId) { + * @param {String} resourceBookingId the resourceBooking id + * @returns {Object} the resourceBooking data + */ +-async function ensureResourceBookingById(resourceBookingId) { +- return models.ResourceBooking.findById(resourceBookingId); ++async function ensureResourceBookingById (resourceBookingId) { ++ return models.ResourceBooking.findById(resourceBookingId) + } + + /** +@@ -1218,8 +1247,8 @@ async function ensureResourceBookingById(resourceBookingId) { + * @param {String} workPeriodId the workPeriod id + * @returns the workPeriod data + */ +-async function ensureWorkPeriodById(workPeriodId) { +- return models.WorkPeriod.findById(workPeriodId); ++async function ensureWorkPeriodById (workPeriodId) { ++ return models.WorkPeriod.findById(workPeriodId) + } + + /** +@@ -1228,24 +1257,24 @@ async function ensureWorkPeriodById(workPeriodId) { + * @param {String} jobId the user id + * @returns {Object} the user data + */ +-async function ensureUserById(userId) { +- const token = await getM2MUbahnToken(); ++async function ensureUserById (userId) { ++ const token = await getM2MUbahnToken() + try { + const res = await request + .get(`${config.TC_API}/users/${userId}`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'ensureUserById', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return res.body; ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return res.body + } catch (err) { + if (err.status === HttpStatus.NOT_FOUND) { +- throw new errors.NotFoundError(`id: ${userId} "user" not found`); ++ throw new errors.NotFoundError(`id: ${userId} "user" not found`) + } +- throw err; ++ throw err + } + } + +@@ -1254,12 +1283,12 @@ async function ensureUserById(userId) { + * + * @returns {Object} the M2M auth user + */ +-function getAuditM2Muser() { ++function getAuditM2Muser () { + return { + isMachine: true, + userId: config.m2m.M2M_AUDIT_USER_ID, +- handle: config.m2m.M2M_AUDIT_HANDLE, +- }; ++ handle: config.m2m.M2M_AUDIT_HANDLE ++ } + } + + /** +@@ -1271,24 +1300,24 @@ function getAuditM2Muser() { + * @param {Number} projectId project id + * @returns the result + */ +-async function checkIsMemberOfProject(userId, projectId) { +- const m2mToken = await getM2MToken(); ++async function checkIsMemberOfProject (userId, projectId) { ++ const m2mToken = await getM2MToken() + const res = await request + .get(`${config.TC_API}/projects/${projectId}`) + .set('Authorization', `Bearer ${m2mToken}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); +- const memberIdList = _.map(res.body.members, 'userId'); ++ .set('Accept', 'application/json') ++ const memberIdList = _.map(res.body.members, 'userId') + localLogger.debug({ + context: 'checkIsMemberOfProject', + message: `the members of project ${projectId}: ${JSON.stringify( + memberIdList +- )}, authUserId: ${JSON.stringify(userId)}`, +- }); ++ )}, authUserId: ${JSON.stringify(userId)}` ++ }) + if (!memberIdList.includes(userId)) { + throw new errors.UnauthorizedError( + `userId: ${userId} the user is not a member of project ${projectId}` +- ); ++ ) + } + } + +@@ -1298,11 +1327,11 @@ async function checkIsMemberOfProject(userId, projectId) { + * @param {Array} handles the array of handles + * @returns {Array} the member details + */ +-async function getMemberDetailsByHandles(handles) { ++async function getMemberDetailsByHandles (handles) { + if (!handles.length) { +- return []; ++ return [] + } +- const token = await getM2MToken(); ++ const token = await getM2MToken() + const res = await request + .get(`${config.TOPCODER_MEMBERS_API}/_search`) + .query({ +@@ -1310,15 +1339,15 @@ async function getMemberDetailsByHandles(handles) { + handles, + (handle) => `handleLower:${handle.toLowerCase()}` + ).join(' OR '), +- fields: 'userId,handle,firstName,lastName,email', ++ fields: 'userId,handle,firstName,lastName,email' + }) + .set('Authorization', `Bearer ${token}`) +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getMemberDetailsByHandles', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return _.get(res.body, 'result.content'); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return _.get(res.body, 'result.content') + } + + /** +@@ -1327,17 +1356,17 @@ async function getMemberDetailsByHandles(handles) { + * @param {String} handle the user handle + * @returns {Object} the member details + */ +-async function getV3MemberDetailsByHandle(handle) { +- const token = await getM2MToken(); ++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'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getV3MemberDetailsByHandle', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return _.get(res.body, 'result.content'); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return _.get(res.body, 'result.content') + } + + /** +@@ -1347,20 +1376,20 @@ async function getV3MemberDetailsByHandle(handle) { + * @param {String} email the email + * @returns {Array} the member details + */ +-async function _getMemberDetailsByEmail(token, email) { ++async function _getMemberDetailsByEmail (token, email) { + const res = await request + .get(config.TOPCODER_USERS_API) + .query({ + filter: `email=${email}`, +- fields: 'handle,id,email,firstName,lastName', ++ fields: 'handle,id,email,firstName,lastName' + }) + .set('Authorization', `Bearer ${token}`) +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: '_getMemberDetailsByEmail', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return _.get(res.body, 'result.content'); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return _.get(res.body, 'result.content') + } + + /** +@@ -1370,25 +1399,25 @@ async function _getMemberDetailsByEmail(token, email) { + * @param {Array} emails the array of emails + * @returns {Array} the member details + */ +-async function getMemberDetailsByEmails(emails) { +- const token = await getM2MToken(); ++async function getMemberDetailsByEmails (emails) { ++ const token = await getM2MToken() + const limiter = new Bottleneck({ +- maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API, +- }); ++ maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API ++ }) + const membersArray = await Promise.all( + emails.map((email) => + limiter.schedule(() => + _getMemberDetailsByEmail(token, email).catch((error) => { + localLogger.error({ + context: 'getMemberDetailsByEmails', +- message: error.message, +- }); +- return []; ++ message: error.message ++ }) ++ return [] + }) + ) + ) +- ); +- return _.flatten(membersArray); ++ ) ++ return _.flatten(membersArray) + } + + /** +@@ -1399,20 +1428,20 @@ async function getMemberDetailsByEmails(emails) { + * @param {Object} criteria the filtering criteria + * @returns {Object} the member created + */ +-async function createProjectMember(projectId, data, criteria) { +- const m2mToken = await getM2MToken(); ++async function createProjectMember (projectId, data, criteria) { ++ const m2mToken = await getM2MToken() + const { body: member } = await request + .post(`${config.TC_API}/projects/${projectId}/members`) + .set('Authorization', `Bearer ${m2mToken}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .query(criteria) +- .send(data); ++ .send(data) + localLogger.debug({ + context: 'createProjectMember', +- message: `response body: ${JSON.stringify(member)}`, +- }); +- return member; ++ message: `response body: ${JSON.stringify(member)}` ++ }) ++ return member + } + + /** +@@ -1422,21 +1451,21 @@ async function createProjectMember(projectId, data, criteria) { + * @param {Object} criteria the search criteria + * @returns {Array} the project members + */ +-async function listProjectMembers(currentUser, projectId, criteria = {}) { ++async function listProjectMembers (currentUser, projectId, criteria = {}) { + const token = + currentUser.hasManagePermission || currentUser.isMachine + ? `Bearer ${await getM2MToken()}` +- : currentUser.jwtToken; ++ : currentUser.jwtToken + const { body: members } = await request + .get(`${config.TC_API}/projects/${projectId}/members`) + .query(criteria) + .set('Authorization', token) +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'listProjectMembers', +- message: `response body: ${JSON.stringify(members)}`, +- }); +- return members; ++ message: `response body: ${JSON.stringify(members)}` ++ }) ++ return members + } + + /** +@@ -1446,21 +1475,21 @@ async function listProjectMembers(currentUser, projectId, criteria = {}) { + * @param {Object} criteria the search criteria + * @returns {Array} the member invites + */ +-async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { ++async function listProjectMemberInvites (currentUser, projectId, criteria = {}) { + const token = + currentUser.hasManagePermission || currentUser.isMachine + ? `Bearer ${await getM2MToken()}` +- : currentUser.jwtToken; ++ : currentUser.jwtToken + const { body: invites } = await request + .get(`${config.TC_API}/projects/${projectId}/invites`) + .query(criteria) + .set('Authorization', token) +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'listProjectMemberInvites', +- message: `response body: ${JSON.stringify(invites)}`, +- }); +- return invites; ++ message: `response body: ${JSON.stringify(invites)}` ++ }) ++ return invites + } + + /** +@@ -1470,24 +1499,24 @@ async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { + * @param {String} projectMemberId the id of the project member + * @returns {undefined} + */ +-async function deleteProjectMember(currentUser, projectId, projectMemberId) { ++async function deleteProjectMember (currentUser, projectId, projectMemberId) { + const token = + currentUser.hasManagePermission || currentUser.isMachine + ? `Bearer ${await getM2MToken()}` +- : currentUser.jwtToken; ++ : currentUser.jwtToken + try { + await request + .delete( + `${config.TC_API}/projects/${projectId}/members/${projectMemberId}` + ) +- .set('Authorization', token); ++ .set('Authorization', token) + } catch (err) { + if (err.status === HttpStatus.NOT_FOUND) { + throw new errors.NotFoundError( + `projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}` +- ); ++ ) + } +- throw err; ++ throw err + } + } + +@@ -1497,13 +1526,13 @@ async function deleteProjectMember(currentUser, projectId, projectMemberId) { + * @param {String} attributeName Requested attribute name, e.g. "email" + * @returns attribute value + */ +-function getUserAttributeValue(user, attributeName) { +- const attributes = _.get(user, 'attributes', []); ++function getUserAttributeValue (user, attributeName) { ++ const attributes = _.get(user, 'attributes', []) + const targetAttribute = _.find( + attributes, + (a) => a.attribute.name === attributeName +- ); +- return _.get(targetAttribute, 'value'); ++ ) ++ return _.get(targetAttribute, 'value') + } + + /** +@@ -1513,34 +1542,34 @@ function getUserAttributeValue(user, attributeName) { + * @param {String} token m2m token + * @returns {Object} the challenge created + */ +-async function createChallenge(data, token) { ++async function createChallenge (data, token) { + if (!token) { +- token = await getM2MToken(); ++ token = await getM2MToken() + } +- const url = `${config.TC_API}/challenges`; ++ const url = `${config.TC_API}/challenges` + localLogger.debug({ + context: 'createChallenge', +- message: `EndPoint: POST ${url}`, +- }); ++ message: `EndPoint: POST ${url}` ++ }) + localLogger.debug({ + context: 'createChallenge', +- message: `Request Body: ${JSON.stringify(data)}`, +- }); ++ message: `Request Body: ${JSON.stringify(data)}` ++ }) + const { body: challenge, status: httpStatus } = await request + .post(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') +- .send(data); ++ .send(data) + localLogger.debug({ + context: 'createChallenge', +- message: `Status Code: ${httpStatus}`, +- }); ++ message: `Status Code: ${httpStatus}` ++ }) + localLogger.debug({ + context: 'createChallenge', +- message: `Response Body: ${JSON.stringify(challenge)}`, +- }); +- return challenge; ++ message: `Response Body: ${JSON.stringify(challenge)}` ++ }) ++ return challenge + } + + /** +@@ -1551,34 +1580,34 @@ async function createChallenge(data, token) { + * @param {String} token m2m token + * @returns {Object} the challenge updated + */ +-async function updateChallenge(challengeId, data, token) { ++async function updateChallenge (challengeId, data, token) { + if (!token) { +- token = await getM2MToken(); ++ token = await getM2MToken() + } +- const url = `${config.TC_API}/challenges/${challengeId}`; ++ const url = `${config.TC_API}/challenges/${challengeId}` + localLogger.debug({ + context: 'updateChallenge', +- message: `EndPoint: PATCH ${url}`, +- }); ++ message: `EndPoint: PATCH ${url}` ++ }) + localLogger.debug({ + context: 'updateChallenge', +- message: `Request Body: ${JSON.stringify(data)}`, +- }); ++ message: `Request Body: ${JSON.stringify(data)}` ++ }) + const { body: challenge, status: httpStatus } = await request + .patch(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') +- .send(data); ++ .send(data) + localLogger.debug({ + context: 'updateChallenge', +- message: `Status Code: ${httpStatus}`, +- }); ++ message: `Status Code: ${httpStatus}` ++ }) + localLogger.debug({ + context: 'updateChallenge', +- message: `Response Body: ${JSON.stringify(challenge)}`, +- }); +- return challenge; ++ message: `Response Body: ${JSON.stringify(challenge)}` ++ }) ++ return challenge + } + + /** +@@ -1588,34 +1617,34 @@ async function updateChallenge(challengeId, data, token) { + * @param {String} token m2m token + * @returns {Object} the resource created + */ +-async function createChallengeResource(data, token) { ++async function createChallengeResource (data, token) { + if (!token) { +- token = await getM2MToken(); ++ token = await getM2MToken() + } +- const url = `${config.TC_API}/resources`; ++ const url = `${config.TC_API}/resources` + localLogger.debug({ + context: 'createChallengeResource', +- message: `EndPoint: POST ${url}`, +- }); ++ message: `EndPoint: POST ${url}` ++ }) + localLogger.debug({ + context: 'createChallengeResource', +- message: `Request Body: ${JSON.stringify(data)}`, +- }); ++ message: `Request Body: ${JSON.stringify(data)}` ++ }) + const { body: resource, status: httpStatus } = await request + .post(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') +- .send(data); ++ .send(data) + localLogger.debug({ + context: 'createChallengeResource', +- message: `Status Code: ${httpStatus}`, +- }); ++ message: `Status Code: ${httpStatus}` ++ }) + localLogger.debug({ + context: 'createChallengeResource', +- message: `Response Body: ${JSON.stringify(resource)}`, +- }); +- return resource; ++ message: `Response Body: ${JSON.stringify(resource)}` ++ }) ++ return resource + } + + /** +@@ -1624,40 +1653,40 @@ async function createChallengeResource(data, token) { + * @param {Date} end end date of the resource booking + * @returns {Array<{startDate:Date, endDate:Date, daysWorked:number}>} information about workPeriods + */ +-function extractWorkPeriods(start, end) { ++function extractWorkPeriods (start, end) { + // calculate maximum possible daysWorked for a week +- function getDaysWorked(week) { ++ function getDaysWorked (week) { + if (weeks === 1) { +- return Math.min(endDay, 5) - Math.max(startDay, 1) + 1; ++ return Math.min(endDay, 5) - Math.max(startDay, 1) + 1 + } else if (week === 0) { +- return Math.min(6 - startDay, 5); ++ return Math.min(6 - startDay, 5) + } else if (week === weeks - 1) { +- return Math.min(endDay, 5); +- } else return 5; ++ return Math.min(endDay, 5) ++ } else return 5 + } +- const periods = []; ++ const periods = [] + if (_.isNil(start) || _.isNil(end)) { +- return periods; ++ return periods + } +- const startDate = moment(start); +- const startDay = startDate.get('day'); +- startDate.set('day', 0).startOf('day'); ++ const startDate = moment(start) ++ const startDay = startDate.get('day') ++ startDate.set('day', 0).startOf('day') + +- const endDate = moment(end); +- const endDay = endDate.get('day'); +- endDate.set('day', 6).endOf('day'); ++ const endDate = moment(end) ++ const endDay = endDate.get('day') ++ endDate.set('day', 6).endOf('day') + +- const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7; ++ const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7 + + for (let i = 0; i < weeks; i++) { + periods.push({ + startDate: startDate.format('YYYY-MM-DD'), + endDate: startDate.add(6, 'day').format('YYYY-MM-DD'), +- daysWorked: getDaysWorked(i), +- }); +- startDate.add(1, 'day'); ++ daysWorked: getDaysWorked(i) ++ }) ++ startDate.add(1, 'day') + } +- return periods; ++ return periods + } + + /** +@@ -1666,19 +1695,19 @@ function extractWorkPeriods(start, end) { + * @param {String} userHandle user handle + * @returns {String} email address of the user + */ +-async function getUserByHandle(userHandle) { +- const token = await getM2MToken(); +- const url = `${config.TC_API}/members/${userHandle}`; ++async function getUserByHandle (userHandle) { ++ const token = await getM2MToken() ++ const url = `${config.TC_API}/members/${userHandle}` + const res = await request + .get(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') +- .set('Accept', 'application/json'); ++ .set('Accept', 'application/json') + localLogger.debug({ + context: 'getUserByHandle', +- message: `response body: ${JSON.stringify(res.body)}`, +- }); +- return _.get(res, 'body'); ++ message: `response body: ${JSON.stringify(res.body)}` ++ }) ++ return _.get(res, 'body') + } + + /** +@@ -1687,14 +1716,14 @@ async function getUserByHandle(userHandle) { + * @param {*} object of json that would be replaced in string + * @returns + */ +-async function substituteStringByObject(string, object) { ++async function substituteStringByObject (string, object) { + for (var key in object) { + if (!Object.prototype.hasOwnProperty.call(object, key)) { +- continue; ++ continue + } +- string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]); ++ string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]) + } +- return string; ++ return string + } + + /** +@@ -1702,19 +1731,19 @@ async function substituteStringByObject(string, object) { + * @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 (currentUser, data) { ++ const token = currentUser.jwtToken + const res = await request + .post(`${config.TC_API}/projects/`) + .set('Authorization', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') +- .send(data); ++ .send(data) + localLogger.debug({ + context: 'createProject', +- message: `response body: ${JSON.stringify(res)}`, +- }); +- return _.get(res, 'body'); ++ message: `response body: ${JSON.stringify(res)}` ++ }) ++ return _.get(res, 'body') + } + + module.exports = { +@@ -1733,9 +1762,9 @@ module.exports = { + getUserId: async (userId) => { + // check m2m user id + if (userId === config.m2m.M2M_AUDIT_USER_ID) { +- return config.m2m.M2M_AUDIT_USER_ID; ++ return config.m2m.M2M_AUDIT_USER_ID + } +- return ensureUbahnUserId({ userId }); ++ return ensureUbahnUserId({ userId }) + }, + getUserByExternalId, + getM2MToken, +@@ -1769,5 +1798,5 @@ module.exports = { + extractWorkPeriods, + getUserByHandle, + substituteStringByObject, +- createProject, +-}; ++ createProject ++} +diff --git a/src/controllers/RoleController.js b/src/controllers/RoleController.js +new file mode 100644 +index 0000000..747cbe4 +--- /dev/null ++++ b/src/controllers/RoleController.js +@@ -0,0 +1,59 @@ ++/** ++ * Controller for Role endpoints ++ */ ++const HttpStatus = require('http-status-codes') ++const service = require('../services/RoleService') ++ ++/** ++ * Get role by id ++ * @param req the request ++ * @param res the response ++ */ ++async function getRole (req, res) { ++ res.send(await service.getRole(req.authUser, req.params.id, req.query.fromDb)) ++} ++ ++/** ++ * Create role ++ * @param req the request ++ * @param res the response ++ */ ++async function createRole (req, res) { ++ res.send(await service.createRole(req.authUser, req.body)) ++} ++ ++/** ++ * update role by id ++ * @param req the request ++ * @param res the response ++ */ ++async function updateRole (req, res) { ++ res.send(await service.updateRole(req.authUser, req.params.id, req.body)) ++} ++ ++/** ++ * Delete role by id ++ * @param req the request ++ * @param res the response ++ */ ++async function deleteRole (req, res) { ++ await service.deleteRole(req.authUser, req.params.id) ++ res.status(HttpStatus.NO_CONTENT).end() ++} ++ ++/** ++ * Search roles ++ * @param req the request ++ * @param res the response ++ */ ++async function searchRoles (req, res) { ++ res.send(await service.searchRoles(req.authUser, req.query)) ++} ++ ++module.exports = { ++ getRole, ++ createRole, ++ updateRole, ++ deleteRole, ++ searchRoles ++} +diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js +index ca4f1bc..26d7073 100644 +--- a/src/controllers/TeamController.js ++++ b/src/controllers/TeamController.js +@@ -1,19 +1,19 @@ + /** + * Controller for TaaS teams endpoints + */ +-const HttpStatus = require('http-status-codes'); +-const service = require('../services/TeamService'); +-const helper = require('../common/helper'); ++const HttpStatus = require('http-status-codes') ++const service = require('../services/TeamService') ++const helper = require('../common/helper') + + /** + * Search teams + * @param req the request + * @param res the response + */ +-async function searchTeams(req, res) { +- const result = await service.searchTeams(req.authUser, req.query); +- helper.setResHeaders(req, res, result); +- res.send(result.result); ++async function searchTeams (req, res) { ++ const result = await service.searchTeams(req.authUser, req.query) ++ helper.setResHeaders(req, res, result) ++ res.send(result.result) + } + + /** +@@ -21,8 +21,8 @@ async function searchTeams(req, res) { + * @param req the request + * @param res the response + */ +-async function getTeam(req, res) { +- res.send(await service.getTeam(req.authUser, req.params.id)); ++async function getTeam (req, res) { ++ res.send(await service.getTeam(req.authUser, req.params.id)) + } + + /** +@@ -30,10 +30,10 @@ async function getTeam(req, res) { + * @param req the request + * @param res the response + */ +-async function getTeamJob(req, res) { ++async function getTeamJob (req, res) { + res.send( + await service.getTeamJob(req.authUser, req.params.id, req.params.jobId) +- ); ++ ) + } + + /** +@@ -41,9 +41,9 @@ async function getTeamJob(req, res) { + * @param req the request + * @param res the response + */ +-async function sendEmail(req, res) { +- await service.sendEmail(req.authUser, req.body); +- res.status(HttpStatus.NO_CONTENT).end(); ++async function sendEmail (req, res) { ++ await service.sendEmail(req.authUser, req.body) ++ res.status(HttpStatus.NO_CONTENT).end() + } + + /** +@@ -51,10 +51,10 @@ async function sendEmail(req, res) { + * @param req the request + * @param res the response + */ +-async function addMembers(req, res) { ++async function addMembers (req, res) { + res.send( + await service.addMembers(req.authUser, req.params.id, req.query, req.body) +- ); ++ ) + } + + /** +@@ -62,13 +62,13 @@ async function addMembers(req, res) { + * @param req the request + * @param res the response + */ +-async function searchMembers(req, res) { ++async function searchMembers (req, res) { + const result = await service.searchMembers( + req.authUser, + req.params.id, + req.query +- ); +- res.send(result.result); ++ ) ++ res.send(result.result) + } + + /** +@@ -76,13 +76,13 @@ async function searchMembers(req, res) { + * @param req the request + * @param res the response + */ +-async function searchInvites(req, res) { ++async function searchInvites (req, res) { + const result = await service.searchInvites( + req.authUser, + req.params.id, + req.query +- ); +- res.send(result.result); ++ ) ++ res.send(result.result) + } + + /** +@@ -90,13 +90,13 @@ async function searchInvites(req, res) { + * @param req the request + * @param res the response + */ +-async function deleteMember(req, res) { ++async function deleteMember (req, res) { + await service.deleteMember( + req.authUser, + req.params.id, + req.params.projectMemberId +- ); +- res.status(HttpStatus.NO_CONTENT).end(); ++ ) ++ res.status(HttpStatus.NO_CONTENT).end() + } + + /** +@@ -104,8 +104,8 @@ async function deleteMember(req, res) { + * @param req the request + * @param res the response + */ +-async function getMe(req, res) { +- res.send(await service.getMe(req.authUser)); ++async function getMe (req, res) { ++ res.send(await service.getMe(req.authUser)) + } + + /** +@@ -113,8 +113,8 @@ async function getMe(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 createProj (req, res) { ++ res.send(await service.createProj(req.authUser, req.body)) + } + + module.exports = { +@@ -127,5 +127,5 @@ module.exports = { + searchInvites, + deleteMember, + getMe, +- createProj, +-}; ++ createProj ++} +diff --git a/src/eventHandlers/RoleEventHandler.js b/src/eventHandlers/RoleEventHandler.js +new file mode 100644 +index 0000000..38dbdb7 +--- /dev/null ++++ b/src/eventHandlers/RoleEventHandler.js +@@ -0,0 +1,64 @@ ++/* ++ * Handle events for ResourceBooking. ++ */ ++ ++const { Op } = require('sequelize') ++const _ = require('lodash') ++const models = require('../models') ++const logger = require('../common/logger') ++const helper = require('../common/helper') ++const JobService = require('../services/JobService') ++ ++const Job = models.Job ++ ++/** ++ * When a Role is deleted, jobs related to ++ * that role should be updated ++ * @param {object} payload the event payload ++ * @returns {undefined} ++ */ ++async function updateJobs (payload) { ++ // find jobs have this role ++ const jobs = await Job.findAll({ ++ where: { ++ roleIds: { [Op.contains]: [payload.value.id] } ++ }, ++ raw: true ++ }) ++ if (jobs.length === 0) { ++ logger.debug({ ++ component: 'RoleEventHandler', ++ context: 'updateJobs', ++ message: `id: ${payload.value.id} role has no related job - ignored` ++ }) ++ return ++ } ++ const m2mUser = helper.getAuditM2Muser() ++ // remove role id from related jobs ++ await Promise.all(_.map(jobs, async job => { ++ let roleIds = _.filter(job.roleIds, roleId => roleId !== payload.value.id) ++ if (roleIds.length === 0) { ++ roleIds = null ++ } ++ await JobService.partiallyUpdateJob(m2mUser, job.id, { roleIds }) ++ })) ++ logger.debug({ ++ component: 'RoleEventHandler', ++ context: 'updateJobs', ++ message: `role id: ${payload.value.id} removed from jobs with id: ${_.map(jobs, 'id')}` ++ }) ++} ++ ++/** ++ * Process role delete event. ++ * ++ * @param {Object} payload the event payload ++ * @returns {undefined} ++ */ ++async function processDelete (payload) { ++ await updateJobs(payload) ++} ++ ++module.exports = { ++ processDelete ++} +diff --git a/src/eventHandlers/index.js b/src/eventHandlers/index.js +index 1744599..6e0ec2a 100644 +--- a/src/eventHandlers/index.js ++++ b/src/eventHandlers/index.js +@@ -8,6 +8,7 @@ const JobEventHandler = require('./JobEventHandler') + const JobCandidateEventHandler = require('./JobCandidateEventHandler') + const ResourceBookingEventHandler = require('./ResourceBookingEventHandler') + const InterviewEventHandler = require('./InterviewEventHandler') ++const RoleEventHandler = require('./RoleEventHandler') + const logger = require('../common/logger') + + const TopicOperationMapping = { +@@ -16,7 +17,8 @@ const TopicOperationMapping = { + [config.TAAS_RESOURCE_BOOKING_CREATE_TOPIC]: ResourceBookingEventHandler.processCreate, + [config.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC]: ResourceBookingEventHandler.processUpdate, + [config.TAAS_RESOURCE_BOOKING_DELETE_TOPIC]: ResourceBookingEventHandler.processDelete, +- [config.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewEventHandler.processRequest ++ [config.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewEventHandler.processRequest, ++ [config.TAAS_ROLE_DELETE_TOPIC]: RoleEventHandler.processDelete + } + + /** +diff --git a/src/models/Job.js b/src/models/Job.js +index 49d34ff..66f15b0 100644 +--- a/src/models/Job.js ++++ b/src/models/Job.js +@@ -104,6 +104,12 @@ module.exports = (sequelize) => { + defaultValue: false, + allowNull: false + }, ++ roleIds: { ++ field: 'role_ids', ++ type: Sequelize.ARRAY({ ++ type: Sequelize.UUID ++ }) ++ }, + createdBy: { + field: 'created_by', + type: Sequelize.UUID, +diff --git a/src/models/Role.js b/src/models/Role.js +new file mode 100644 +index 0000000..57cd502 +--- /dev/null ++++ b/src/models/Role.js +@@ -0,0 +1,165 @@ ++const { Sequelize, Model } = require('sequelize') ++const config = require('config') ++const errors = require('../common/errors') ++ ++module.exports = (sequelize) => { ++ class Role extends Model { ++ /** ++ * Get role by id ++ * @param {String} id the role id ++ * @returns {Role} the role instance ++ */ ++ static async findById (id) { ++ const role = await Role.findOne({ ++ where: { ++ id ++ } ++ }) ++ if (!role) { ++ throw new errors.NotFoundError(`id: ${id} "Role" doesn't exists.`) ++ } ++ return role ++ } ++ } ++ Role.init( ++ { ++ id: { ++ type: Sequelize.UUID, ++ primaryKey: true, ++ allowNull: false, ++ defaultValue: Sequelize.UUIDV4 ++ }, ++ name: { ++ type: Sequelize.STRING(50), ++ allowNull: false ++ }, ++ description: { ++ type: Sequelize.STRING(1000) ++ }, ++ listOfSkills: { ++ field: 'list_of_skills', ++ type: Sequelize.ARRAY({ ++ type: Sequelize.STRING(50) ++ }) ++ }, ++ rates: { ++ type: Sequelize.ARRAY({ ++ type: Sequelize.JSONB({ ++ global: { ++ type: Sequelize.SMALLINT, ++ allowNull: false ++ }, ++ inCountry: { ++ field: 'in_country', ++ type: Sequelize.SMALLINT, ++ allowNull: false ++ }, ++ offShore: { ++ field: 'off_shore', ++ type: Sequelize.SMALLINT, ++ allowNull: false ++ }, ++ rate30Global: { ++ field: 'rate30_global', ++ type: Sequelize.SMALLINT ++ }, ++ rate30InCountry: { ++ field: 'rate30_in_country', ++ type: Sequelize.SMALLINT ++ }, ++ rate30OffShore: { ++ field: 'rate30_off_shore', ++ type: Sequelize.SMALLINT ++ }, ++ rate20Global: { ++ field: 'rate20_global', ++ type: Sequelize.SMALLINT ++ }, ++ rate20InCountry: { ++ field: 'rate20_in_country', ++ type: Sequelize.SMALLINT ++ }, ++ rate20OffShore: { ++ field: 'rate20_off_shore', ++ type: Sequelize.SMALLINT ++ } ++ }), ++ allowNull: false ++ }), ++ allowNull: false ++ }, ++ numberOfMembers: { ++ field: 'number_of_members', ++ type: Sequelize.NUMERIC ++ }, ++ numberOfMembersAvailable: { ++ field: 'number_of_members_available', ++ type: Sequelize.SMALLINT ++ }, ++ imageUrl: { ++ field: 'image_url', ++ type: Sequelize.STRING(255) ++ }, ++ timeToCandidate: { ++ field: 'time_to_candidate', ++ type: Sequelize.SMALLINT ++ }, ++ timeToInterview: { ++ field: 'time_to_interview', ++ type: Sequelize.SMALLINT ++ }, ++ 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: 'roles', ++ paranoid: true, ++ deletedAt: 'deletedAt', ++ createdAt: 'createdAt', ++ updatedAt: 'updatedAt', ++ timestamps: true, ++ defaultScope: { ++ attributes: { ++ exclude: ['deletedAt'] ++ } ++ }, ++ hooks: { ++ afterCreate: (role) => { ++ delete role.dataValues.deletedAt ++ } ++ }, ++ indexes: [ ++ { ++ unique: true, ++ fields: ['name'], ++ where: { ++ deleted_at: null ++ } ++ } ++ ] ++ } ++ ) ++ ++ return Role ++} +diff --git a/src/routes/RoleRoutes.js b/src/routes/RoleRoutes.js +new file mode 100644 +index 0000000..2fb6d55 +--- /dev/null ++++ b/src/routes/RoleRoutes.js +@@ -0,0 +1,41 @@ ++/** ++ * Contains role routes ++ */ ++const constants = require('../../app-constants') ++ ++module.exports = { ++ '/roles': { ++ post: { ++ controller: 'RoleController', ++ method: 'createRole', ++ auth: 'jwt', ++ scopes: [constants.Scopes.CREATE_ROLE, constants.Scopes.ALL_ROLE] ++ }, ++ get: { ++ controller: 'RoleController', ++ method: 'searchRoles', ++ auth: 'jwt', ++ scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] ++ } ++ }, ++ '/roles/:id': { ++ get: { ++ controller: 'RoleController', ++ method: 'getRole', ++ auth: 'jwt', ++ scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] ++ }, ++ patch: { ++ controller: 'RoleController', ++ method: 'updateRole', ++ auth: 'jwt', ++ scopes: [constants.Scopes.UPDATE_ROLE, constants.Scopes.ALL_ROLE] ++ }, ++ delete: { ++ controller: 'RoleController', ++ method: 'deleteRole', ++ auth: 'jwt', ++ scopes: [constants.Scopes.DELETE_ROLE, constants.Scopes.ALL_ROLE] ++ } ++ } ++} +diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js +index 9bbe25c..07d777d 100644 +--- a/src/routes/TeamRoutes.js ++++ b/src/routes/TeamRoutes.js +@@ -1,7 +1,7 @@ + /** + * Contains taas team routes + */ +-const constants = require('../../app-constants'); ++const constants = require('../../app-constants') + + module.exports = { + '/taas-teams': { +@@ -9,85 +9,85 @@ module.exports = { + controller: 'TeamController', + method: 'searchTeams', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/email': { + post: { + controller: 'TeamController', + method: 'sendEmail', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/skills': { + get: { + controller: 'SkillController', + method: 'searchSkills', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/me': { + get: { + controller: 'TeamController', + method: 'getMe', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/:id': { + get: { + controller: 'TeamController', + method: 'getTeam', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/:id/jobs/:jobId': { + get: { + controller: 'TeamController', + method: 'getTeamJob', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/:id/members': { + post: { + controller: 'TeamController', + method: 'addMembers', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], ++ scopes: [constants.Scopes.READ_TAAS_TEAM] + }, + get: { + controller: 'TeamController', + method: 'searchMembers', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/:id/invites': { + get: { + controller: 'TeamController', + method: 'searchInvites', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/:id/members/:projectMemberId': { + delete: { + controller: 'TeamController', + method: 'deleteMember', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } + }, + '/taas-teams/createTeamRequest': { + post: { + controller: 'TeamController', + method: 'createProj', + auth: 'jwt', +- scopes: [constants.Scopes.READ_TAAS_TEAM], +- }, +- }, +-}; ++ scopes: [constants.Scopes.READ_TAAS_TEAM] ++ } ++ } ++} +diff --git a/src/services/InterviewService.js b/src/services/InterviewService.js +index 10a065f..a69a788 100644 +--- a/src/services/InterviewService.js ++++ b/src/services/InterviewService.js +@@ -241,8 +241,8 @@ async function requestInterview (currentUser, jobCandidateId, interview) { + 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] ++ var foundGuestMember = _.find(guestMembers, function (guestMember) { return guestEmail === guestMember.email }) ++ return (foundGuestMember !== undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split('@')[0] + }) + + try { +diff --git a/src/services/JobService.js b/src/services/JobService.js +index 7d855bd..be5dfde 100644 +--- a/src/services/JobService.js ++++ b/src/services/JobService.js +@@ -74,6 +74,27 @@ async function _validateSkills (skills) { + } + } + ++/** ++ * Validate if all roles exist. ++ * ++ * @param {Array} roles the list of roles ++ * @returns {undefined} ++ */ ++async function _validateRoles (roles) { ++ const foundRolesObj = await models.Role.findAll({ ++ where: { ++ id: roles ++ }, ++ attributes: ['id'], ++ raw: true ++ }) ++ const foundRoles = _.map(foundRolesObj, 'id') ++ const nonexistentRoles = _.difference(roles, foundRoles) ++ if (nonexistentRoles.length > 0) { ++ throw new errors.BadRequestError(`Invalid roles: [${nonexistentRoles}]`) ++ } ++} ++ + /** + * Check user permission for getting job. + * +@@ -154,6 +175,10 @@ async function createJob (currentUser, job) { + } + + await _validateSkills(job.skills) ++ if (job.roleIds) { ++ job.roleIds = _.uniq(job.roleIds) ++ await _validateRoles(job.roleIds) ++ } + job.id = uuid() + job.createdBy = await helper.getUserId(currentUser.userId) + +@@ -177,7 +202,8 @@ createJob.schema = Joi.object().keys({ + rateType: Joi.rateType().allow(null), + workload: Joi.workload().allow(null), + skills: Joi.array().items(Joi.string().uuid()).required(), +- isApplicationPageActive: Joi.boolean() ++ isApplicationPageActive: Joi.boolean(), ++ roleIds: Joi.array().items(Joi.string().uuid().required()) + }).required() + }).required() + +@@ -192,6 +218,10 @@ async function updateJob (currentUser, id, data) { + if (data.skills) { + await _validateSkills(data.skills) + } ++ if (data.roleIds) { ++ data.roleIds = _.uniq(data.roleIds) ++ await _validateRoles(data.roleIds) ++ } + let job = await Job.findById(id) + const oldValue = job.toJSON() + +@@ -245,7 +275,8 @@ partiallyUpdateJob.schema = Joi.object().keys({ + rateType: Joi.rateType().allow(null), + workload: Joi.workload().allow(null), + skills: Joi.array().items(Joi.string().uuid()), +- isApplicationPageActive: Joi.boolean() ++ isApplicationPageActive: Joi.boolean(), ++ roleIds: Joi.array().items(Joi.string().uuid().required()).allow(null) + }).required() + }).required() + +@@ -276,7 +307,8 @@ fullyUpdateJob.schema = Joi.object().keys({ + workload: Joi.workload().allow(null).default(null), + skills: Joi.array().items(Joi.string().uuid()).required(), + status: Joi.jobStatus().default('sourcing'), +- isApplicationPageActive: Joi.boolean() ++ isApplicationPageActive: Joi.boolean(), ++ roleIds: Joi.array().items(Joi.string().uuid().required()).default(null) + }).required() + }).required() + +@@ -444,9 +476,9 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } + [Op.like]: `%${criteria.title}%` + } + } +- if (criteria.skills) { ++ if (criteria.skill) { + filter.skills = { +- [Op.contains]: [criteria.skills] ++ [Op.contains]: [criteria.skill] + } + } + const jobs = await Job.findAll({ +diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js +index f5c4020..fd3d777 100644 +--- a/src/services/ResourceBookingService.js ++++ b/src/services/ResourceBookingService.js +@@ -1,3 +1,4 @@ ++/* eslint-disable no-unreachable */ + /** + * This service provides operations of ResourceBooking. + */ +diff --git a/src/services/RoleService.js b/src/services/RoleService.js +new file mode 100644 +index 0000000..19006f6 +--- /dev/null ++++ b/src/services/RoleService.js +@@ -0,0 +1,305 @@ ++/** ++ * This service provides operations of Roles. ++ */ ++ ++const _ = require('lodash') ++const config = require('config') ++const Joi = require('joi') ++const { Op } = require('sequelize') ++const uuid = require('uuid') ++const helper = require('../common/helper') ++const logger = require('../common/logger') ++const errors = require('../common/errors') ++const models = require('../models') ++ ++const Role = models.Role ++const esClient = helper.getESClient() ++ ++/** ++ * Check user permission for deleting, creating or updating role. ++ * @param {Object} currentUser the user who perform this operation. ++ * @returns {undefined} ++ */ ++async function _checkUserPermissionForWriteDeleteRole (currentUser) { ++ if (!currentUser.hasManagePermission && !currentUser.isMachine) { ++ throw new errors.ForbiddenError('You are not allowed to perform this action!') ++ } ++} ++ ++/** ++ * Cleans and validates skill names using skills service ++ * @param {Array} skills array of skill names to validate ++ * @returns {undefined} ++ */ ++async function _cleanAndValidateSkillNames (skills) { ++ // remove duplicates, leading and trailing whitespaces, remove empties and convert to lowercase. ++ const cleanedSkills = _.uniq(_.filter(_.map(skills, skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill))) ++ if (cleanedSkills.length > 0) { ++ // search skills if they are exists ++ const { result } = await helper.getTopcoderSkills({ name: _.join(cleanedSkills, ',') }) ++ const skillNames = _.map(result, 'name') ++ // find skills that not valid ++ const unValidSkills = _.differenceWith(cleanedSkills, skillNames, (a, b) => _.toLower(a) === _.toLower(b)) ++ if (unValidSkills.length > 0) { ++ throw new errors.BadRequestError(`skills: "${unValidSkills}" are not valid`) ++ } ++ return cleanedSkills ++ } else { ++ return null ++ } ++} ++ ++/** ++ * Check user permission for deleting, creating or updating role. ++ * @param {Object} currentUser the user who perform this operation. ++ * @returns {undefined} ++ */ ++async function _checkIfSameNamedRoleExists (roleName) { ++ // We can't create another Role with the same name ++ const role = await Role.findOne({ ++ where: { ++ name: { [Op.iLike]: roleName } ++ }, ++ raw: true ++ }) ++ if (role) { ++ throw new errors.BadRequestError(`Role: "${role.name}" is already exists.`) ++ } ++} ++ ++/** ++ * Get role by id ++ * @param {Object} currentUser the user who perform this operation. ++ * @param {String} id the role id ++ * @param {Boolean} fromDb flag if query db for data or not ++ * @returns {Object} the role ++ */ ++async function getRole (currentUser, id, fromDb = false) { ++ if (!fromDb) { ++ try { ++ const role = await esClient.get({ ++ index: config.esConfig.ES_INDEX_ROLE, ++ id ++ }) ++ return { id: role.body._id, ...role.body._source } ++ } catch (err) { ++ if (helper.isDocumentMissingException(err)) { ++ throw new errors.NotFoundError(`id: ${id} "Role" not found`) ++ } ++ } ++ } ++ logger.info({ component: 'RoleService', context: 'getRole', message: 'try to query db for data' }) ++ const role = await Role.findById(id) ++ ++ return role.toJSON() ++} ++ ++getRole.schema = Joi.object().keys({ ++ currentUser: Joi.object().required(), ++ id: Joi.string().uuid().required(), ++ fromDb: Joi.boolean() ++}).required() ++ ++/** ++ * Create role ++ * @param {Object} currentUser the user who perform this operation ++ * @param {Object} role the role to be created ++ * @returns {Object} the created role ++ */ ++async function createRole (currentUser, role) { ++ // check permission ++ await _checkUserPermissionForWriteDeleteRole(currentUser) ++ // check if another Role with the same name exists. ++ await _checkIfSameNamedRoleExists(role.name) ++ // clean and validate skill names ++ if (role.listOfSkills) { ++ role.listOfSkills = await _cleanAndValidateSkillNames(role.listOfSkills) ++ } ++ ++ role.id = uuid.v4() ++ role.createdBy = await helper.getUserId(currentUser.userId) ++ ++ const created = await Role.create(role) ++ ++ await helper.postEvent(config.TAAS_ROLE_CREATE_TOPIC, created.toJSON()) ++ return created.toJSON() ++} ++ ++createRole.schema = Joi.object().keys({ ++ currentUser: Joi.object().required(), ++ role: Joi.object().keys({ ++ name: Joi.string().max(50).required(), ++ description: Joi.string().max(1000), ++ listOfSkills: Joi.array().items(Joi.string().max(50).required()), ++ rates: Joi.array().items(Joi.object().keys({ ++ global: Joi.smallint().required(), ++ inCountry: Joi.smallint().required(), ++ offShore: Joi.smallint().required(), ++ rate30Global: Joi.smallint(), ++ rate30InCountry: Joi.smallint(), ++ rate30OffShore: Joi.smallint(), ++ rate20Global: Joi.smallint(), ++ rate20InCountry: Joi.smallint(), ++ rate20OffShore: Joi.smallint() ++ }).required()).required(), ++ numberOfMembers: Joi.number(), ++ numberOfMembersAvailable: Joi.smallint(), ++ imageUrl: Joi.string().uri().max(255), ++ timeToCandidate: Joi.smallint(), ++ timeToInterview: Joi.smallint() ++ }).required() ++}).required() ++ ++/** ++ * Partially Update role ++ * @param {Object} currentUser the user who perform this operation ++ * @param {String} id the role id ++ * @param {Object} data the data to be updated ++ * @returns {Object} the updated role ++ */ ++async function updateRole (currentUser, id, data) { ++ // check permission ++ await _checkUserPermissionForWriteDeleteRole(currentUser) ++ ++ const role = await Role.findById(id) ++ const oldValue = role.toJSON() ++ // if name is changed, check if another Role with the same name exists. ++ if (data.name && data.name.toLowerCase() !== role.dataValues.name.toLowerCase()) { ++ await _checkIfSameNamedRoleExists(data.name) ++ } ++ // clean and validate skill names ++ if (data.listOfSkills) { ++ data.listOfSkills = await _cleanAndValidateSkillNames(data.listOfSkills) ++ } ++ ++ data.updatedBy = await helper.getUserId(currentUser.userId) ++ const updated = await role.update(data) ++ ++ await helper.postEvent(config.TAAS_ROLE_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) ++ return updated.toJSON() ++} ++ ++updateRole.schema = Joi.object().keys({ ++ currentUser: Joi.object().required(), ++ id: Joi.string().uuid().required(), ++ data: Joi.object().keys({ ++ name: Joi.string().max(50), ++ description: Joi.string().max(1000).allow(null), ++ listOfSkills: Joi.array().items(Joi.string().max(50).required()).allow(null), ++ rates: Joi.array().items(Joi.object().keys({ ++ global: Joi.smallint().required(), ++ inCountry: Joi.smallint().required(), ++ offShore: Joi.smallint().required(), ++ rate30Global: Joi.smallint(), ++ rate30InCountry: Joi.smallint(), ++ rate30OffShore: Joi.smallint(), ++ rate20Global: Joi.smallint(), ++ rate20InCountry: Joi.smallint(), ++ rate20OffShore: Joi.smallint() ++ }).required()), ++ numberOfMembers: Joi.number().allow(null), ++ numberOfMembersAvailable: Joi.smallint().allow(null), ++ imageUrl: Joi.string().uri().max(255).allow(null), ++ timeToCandidate: Joi.smallint().allow(null), ++ timeToInterview: Joi.smallint().allow(null) ++ }).required() ++}).required() ++ ++/** ++ * Delete role by id ++ * @param {Object} currentUser the user who perform this operation ++ * @param {String} id the role id ++ */ ++async function deleteRole (currentUser, id) { ++ // check permission ++ await _checkUserPermissionForWriteDeleteRole(currentUser) ++ ++ const role = await Role.findById(id) ++ await role.destroy() ++ await helper.postEvent(config.TAAS_ROLE_DELETE_TOPIC, { id }) ++} ++ ++deleteRole.schema = Joi.object().keys({ ++ currentUser: Joi.object().required(), ++ id: Joi.string().uuid().required() ++}).required() ++ ++/** ++ * List roles ++ * @param {Object} currentUser the user who perform this operation. ++ * @param {Object} criteria the search criteria ++ * @returns {Object} the search result ++ */ ++async function searchRoles (currentUser, criteria) { ++ // clean skill names and convert into an array ++ criteria.skillsList = _.filter(_.map(_.split(_.trim(criteria.skillsList), ','), skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill)) ++ try { ++ const esQuery = { ++ index: config.get('esConfig.ES_INDEX_ROLE'), ++ body: { ++ query: { ++ bool: { ++ must: [] ++ } ++ }, ++ size: 10000, ++ sort: [{ name: { order: 'asc' } }] ++ } ++ } ++ // Apply skill name filters. listOfSkills array should include all skills provided in criteria. ++ _.each(criteria.skillsList, skill => { ++ esQuery.body.query.bool.must.push({ ++ term: { ++ listOfSkills: skill ++ } ++ }) ++ }) ++ // Apply name filter, allow partial match ++ if (criteria.keyword) { ++ esQuery.body.query.bool.must.push({ ++ wildcard: { ++ name: `*${criteria.keyword}*` ++ ++ } ++ }) ++ } ++ logger.debug({ component: 'RoleService', context: 'searchRoles', message: `Query: ${JSON.stringify(esQuery)}` }) ++ ++ const { body } = await esClient.search(esQuery) ++ return _.map(body.hits.hits, (hit) => _.assign(hit._source, { id: hit._id })) ++ } catch (err) { ++ logger.logFullError(err, { component: 'RoleService', context: 'searchRoles' }) ++ } ++ logger.info({ component: 'RoleService', context: 'searchRoles', message: 'fallback to DB query' }) ++ const filter = { [Op.and]: [] } ++ // Apply skill name filters. listOfSkills array should include all skills provided in criteria. ++ if (criteria.skillsList) { ++ filter[Op.and].push({ listOfSkills: { [Op.contains]: criteria.skillsList } }) ++ } ++ // Apply name filter, allow partial match and ignore case ++ if (criteria.keyword) { ++ filter[Op.and].push({ name: { [Op.iLike]: `%${criteria.keyword}%` } }) ++ } ++ const queryCriteria = { ++ where: filter, ++ order: [['name', 'asc']] ++ } ++ const roles = await Role.findAll(queryCriteria) ++ return roles ++} ++ ++searchRoles.schema = Joi.object().keys({ ++ currentUser: Joi.object().required(), ++ criteria: Joi.object().keys({ ++ skillsList: Joi.string(), ++ keyword: Joi.string() ++ }).required() ++}).required() ++ ++module.exports = { ++ getRole, ++ createRole, ++ updateRole, ++ deleteRole, ++ searchRoles ++} +diff --git a/src/services/TeamService.js b/src/services/TeamService.js +index 3f6dbfd..4052e94 100644 +--- a/src/services/TeamService.js ++++ b/src/services/TeamService.js +@@ -2,16 +2,16 @@ + * This service provides operations of Job. + */ + +-const _ = require('lodash'); +-const Joi = require('joi'); +-const dateFNS = require('date-fns'); +-const config = require('config'); +-const emailTemplateConfig = require('../../config/email_template.config'); +-const helper = require('../common/helper'); +-const logger = require('../common/logger'); +-const errors = require('../common/errors'); +-const JobService = require('./JobService'); +-const ResourceBookingService = require('./ResourceBookingService'); ++const _ = require('lodash') ++const Joi = require('joi') ++const dateFNS = require('date-fns') ++const config = require('config') ++const emailTemplateConfig = require('../../config/email_template.config') ++const helper = require('../common/helper') ++const logger = require('../common/logger') ++const errors = require('../common/errors') ++const JobService = require('./JobService') ++const ResourceBookingService = require('./ResourceBookingService') + + const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { + return { +@@ -20,9 +20,9 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { + from: template.from, + recipients: template.recipients, + cc: template.cc, +- sendgridTemplateId: template.sendgridTemplateId, +- }; +-}); ++ sendgridTemplateId: template.sendgridTemplateId ++ } ++}) + + /** + * Function to get placed resource bookings with specific projectIds +@@ -30,14 +30,14 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { + * @param {Array} projectIds project ids + * @returns the request result + */ +-async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { +- const criteria = { status: 'placed', projectIds }; ++async function _getPlacedResourceBookingsByProjectIds (currentUser, projectIds) { ++ const criteria = { status: 'placed', projectIds } + const { result } = await ResourceBookingService.searchResourceBookings( + currentUser, + criteria, + { returnAll: true } +- ); +- return result; ++ ) ++ return result + } + + /** +@@ -46,13 +46,13 @@ async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { + * @param {Array} projectIds project ids + * @returns the request result + */ +-async function _getJobsByProjectIds(currentUser, projectIds) { ++async function _getJobsByProjectIds (currentUser, projectIds) { + const { result } = await JobService.searchJobs( + currentUser, + { projectIds }, + { returnAll: true } +- ); +- return result; ++ ) ++ return result + } + + /** +@@ -61,26 +61,26 @@ async function _getJobsByProjectIds(currentUser, projectIds) { + * @param {Object} criteria the search criteria + * @returns {Object} the search result, contain total/page/perPage and result array + */ +-async function searchTeams(currentUser, criteria) { +- const sort = `${criteria.sortBy} ${criteria.sortOrder}`; ++async function searchTeams (currentUser, criteria) { ++ const sort = `${criteria.sortBy} ${criteria.sortOrder}` + // Get projects from /v5/projects with searching criteria + const { + total, + page, + perPage, +- result: projects, ++ result: projects + } = await helper.getProjects(currentUser, { + page: criteria.page, + perPage: criteria.perPage, + name: criteria.name, +- sort, +- }); ++ sort ++ }) + return { + total, + page, + perPage, +- result: await getTeamDetail(currentUser, projects), +- }; ++ result: await getTeamDetail(currentUser, projects) ++ } + } + + searchTeams.schema = Joi.object() +@@ -107,13 +107,13 @@ searchTeams.schema = Joi.object() + then: Joi.forbidden().label( + 'sortOrder(with sortBy being `best match`)' + ), +- otherwise: Joi.string().valid('asc', 'desc').default('desc'), ++ otherwise: Joi.string().valid('asc', 'desc').default('desc') + }), +- name: Joi.string(), ++ name: Joi.string() + }) +- .required(), ++ .required() + }) +- .required(); ++ .required() + + /** + * Get team details +@@ -122,69 +122,69 @@ searchTeams.schema = Joi.object() + * @param {Object} isSearch the flag whether for search function + * @returns {Object} the search result + */ +-async function getTeamDetail(currentUser, projects, isSearch = true) { +- const projectIds = _.map(projects, 'id'); ++async function getTeamDetail (currentUser, projects, isSearch = true) { ++ const projectIds = _.map(projects, 'id') + // Get all placed resourceBookings filtered by projectIds + const resourceBookings = await _getPlacedResourceBookingsByProjectIds( + currentUser, + projectIds +- ); ++ ) + // Get all jobs filtered by projectIds +- const jobs = await _getJobsByProjectIds(currentUser, projectIds); ++ const jobs = await _getJobsByProjectIds(currentUser, projectIds) + + // Get first week day and last week day +- const curr = new Date(); +- const firstDay = dateFNS.startOfWeek(curr); +- const lastDay = dateFNS.endOfWeek(curr); ++ const curr = new Date() ++ const firstDay = dateFNS.startOfWeek(curr) ++ const lastDay = dateFNS.endOfWeek(curr) + + logger.debug({ + component: 'TeamService', + context: 'getTeamDetail', +- message: `week started: ${firstDay}, week ended: ${lastDay}`, +- }); ++ message: `week started: ${firstDay}, week ended: ${lastDay}` ++ }) + +- const result = []; ++ const result = [] + for (const project of projects) { +- const rbs = _.filter(resourceBookings, { projectId: project.id }); +- const res = _.clone(project); +- res.weeklyCost = 0; +- res.resources = []; ++ const rbs = _.filter(resourceBookings, { projectId: project.id }) ++ const res = _.clone(project) ++ res.weeklyCost = 0 ++ res.resources = [] + + if (rbs && rbs.length > 0) { + // Get minimal start date and maximal end date +- const startDates = []; +- const endDates = []; ++ const startDates = [] ++ const endDates = [] + for (const rbsItem of rbs) { + if (rbsItem.startDate) { +- startDates.push(new Date(rbsItem.startDate)); ++ startDates.push(new Date(rbsItem.startDate)) + } + if (rbsItem.endDate) { +- endDates.push(new Date(rbsItem.endDate)); ++ endDates.push(new Date(rbsItem.endDate)) + } + } + + if (startDates && startDates.length > 0) { +- res.startDate = _.min(startDates); ++ res.startDate = _.min(startDates) + } + if (endDates && endDates.length > 0) { +- res.endDate = _.max(endDates); ++ res.endDate = _.max(endDates) + } + + // Count weekly rate + for (const item of rbs) { + // ignore any resourceBooking that has customerRate missed + if (!item.customerRate) { +- continue; ++ continue + } +- const startDate = new Date(item.startDate); +- const endDate = new Date(item.endDate); ++ const startDate = new Date(item.startDate) ++ const endDate = new Date(item.endDate) + + // normally startDate is smaller than endDate for a resourceBooking so not check if startDate < endDate + if ( + (!item.startDate || startDate < lastDay) && + (!item.endDate || endDate > firstDay) + ) { +- res.weeklyCost += item.customerRate; ++ res.weeklyCost += item.customerRate + } + } + +@@ -194,48 +194,48 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { + const resource = { + id: rb.id, + userId: user.id, +- ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']), +- }; ++ ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']) ++ } + // If call function is not search, add jobId field + if (!isSearch) { +- resource.jobId = rb.jobId; +- resource.customerRate = rb.customerRate; +- resource.startDate = rb.startDate; +- resource.endDate = rb.endDate; ++ resource.jobId = rb.jobId ++ resource.customerRate = rb.customerRate ++ resource.startDate = rb.startDate ++ resource.endDate = rb.endDate + } +- return resource; +- }); ++ return resource ++ }) + }) +- ); ++ ) + if (resourceInfos && resourceInfos.length > 0) { +- res.resources = resourceInfos; ++ res.resources = resourceInfos + +- const userHandles = _.map(resourceInfos, 'handle'); ++ const userHandles = _.map(resourceInfos, 'handle') + // Get user photo from /v5/members +- const members = await helper.getMembers(userHandles); ++ const members = await helper.getMembers(userHandles) + + for (const item of res.resources) { + const findMember = _.find(members, { +- handleLower: item.handle.toLowerCase(), +- }); ++ handleLower: item.handle.toLowerCase() ++ }) + if (findMember && findMember.photoURL) { +- item.photo_url = findMember.photoURL; ++ item.photo_url = findMember.photoURL + } + } + } + } + +- const jobsTmp = _.filter(jobs, { projectId: project.id }); ++ const jobsTmp = _.filter(jobs, { projectId: project.id }) + if (jobsTmp && jobsTmp.length > 0) { + if (isSearch) { + // Count total positions +- res.totalPositions = 0; ++ res.totalPositions = 0 + for (const item of jobsTmp) { + // only sum numPositions of jobs whose status is NOT cancelled or closed + if (['cancelled', 'closed'].includes(item.status)) { +- continue; ++ continue + } +- res.totalPositions += item.numPositions; ++ res.totalPositions += item.numPositions + } + } else { + res.jobs = _.map(jobsTmp, (job) => { +@@ -249,15 +249,15 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { + 'skills', + 'customerRate', + 'status', +- 'title', +- ]); +- }); ++ 'title' ++ ]) ++ }) + } + } +- result.push(res); ++ result.push(res) + } + +- return result; ++ return result + } + + /** +@@ -266,35 +266,35 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { + * @param {String} id the job id + * @returns {Object} the team + */ +-async function getTeam(currentUser, id) { +- const project = await helper.getProjectById(currentUser, id); +- const result = await getTeamDetail(currentUser, [project], false); +- const teamDetail = result[0]; ++async function getTeam (currentUser, id) { ++ const project = await helper.getProjectById(currentUser, id) ++ const result = await getTeamDetail(currentUser, [project], false) ++ const teamDetail = result[0] + + // add job skills for result +- let jobSkills = []; ++ let jobSkills = [] + if (teamDetail && teamDetail.jobs) { + for (const job of teamDetail.jobs) { + if (job.skills) { +- const usersPromises = []; ++ const usersPromises = [] + _.map(job.skills, (skillId) => { +- usersPromises.push(helper.getSkillById(skillId)); +- }); +- jobSkills = await Promise.all(usersPromises); +- job.skills = jobSkills; ++ usersPromises.push(helper.getSkillById(skillId)) ++ }) ++ jobSkills = await Promise.all(usersPromises) ++ job.skills = jobSkills + } + } + } + +- return teamDetail; ++ return teamDetail + } + + getTeam.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), +- id: Joi.number().integer().required(), ++ id: Joi.number().integer().required() + }) +- .required(); ++ .required() + + /** + * Get team job with id +@@ -303,25 +303,25 @@ getTeam.schema = Joi.object() + * @param {String} jobId the job id + * @returns the team job + */ +-async function getTeamJob(currentUser, id, jobId) { +- const project = await helper.getProjectById(currentUser, id); +- const jobs = await _getJobsByProjectIds(currentUser, [project.id]); +- const job = _.find(jobs, { id: jobId }); ++async function getTeamJob (currentUser, id, jobId) { ++ const project = await helper.getProjectById(currentUser, id) ++ const jobs = await _getJobsByProjectIds(currentUser, [project.id]) ++ const job = _.find(jobs, { id: jobId }) + + if (!job) { + throw new errors.NotFoundError( + `id: ${jobId} "Job" with Team id ${id} doesn't exist` +- ); ++ ) + } + const result = { + id: job.id, +- title: job.title, +- }; ++ title: job.title ++ } + + if (job.skills) { + result.skills = await Promise.all( + _.map(job.skills, (skillId) => helper.getSkillById(skillId)) +- ); ++ ) + } + + // If the job has candidates, the following data for each candidate would be populated: +@@ -336,12 +336,12 @@ async function getTeamJob(currentUser, id, jobId) { + _.map(_.uniq(_.map(job.candidates, 'userId')), (userId) => + helper.getUserById(userId, true) + ) +- ); +- const userMap = _.groupBy(users, 'id'); ++ ) ++ const userMap = _.groupBy(users, 'id') + + // find photo URLs for users +- const members = await helper.getMembers(_.map(users, 'handle')); +- const photoURLMap = _.groupBy(members, 'handleLower'); ++ const members = await helper.getMembers(_.map(users, 'handle')) ++ const photoURLMap = _.groupBy(members, 'handleLower') + + result.candidates = _.map(job.candidates, (candidate) => { + const candidateData = _.pick(candidate, [ +@@ -349,33 +349,33 @@ async function getTeamJob(currentUser, id, jobId) { + 'resume', + 'userId', + 'interviews', +- 'id', +- ]); +- const userData = userMap[candidate.userId][0]; ++ 'id' ++ ]) ++ const userData = userMap[candidate.userId][0] + // attach user data to the candidate + Object.assign( + candidateData, + _.pick(userData, ['handle', 'firstName', 'lastName', 'skills']) +- ); ++ ) + // attach photo URL to the candidate +- const handleLower = userData.handle.toLowerCase(); ++ const handleLower = userData.handle.toLowerCase() + if (photoURLMap[handleLower]) { +- candidateData.photo_url = photoURLMap[handleLower][0].photoURL; ++ candidateData.photo_url = photoURLMap[handleLower][0].photoURL + } +- return candidateData; +- }); ++ return candidateData ++ }) + } + +- return result; ++ return result + } + + getTeamJob.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), +- jobId: Joi.string().guid().required(), ++ jobId: Joi.string().guid().required() + }) +- .required(); ++ .required() + + /** + * Send email through a particular template +@@ -383,21 +383,21 @@ getTeamJob.schema = Joi.object() + * @param {Object} data the email object + * @returns {undefined} + */ +-async function sendEmail(currentUser, data) { +- const template = emailTemplates[data.template]; +- const dataCC = data.cc || []; +- const templateCC = template.cc || []; +- const dataRecipients = data.recipients || []; +- const templateRecipients = template.recipients || []; ++async function sendEmail (currentUser, data) { ++ const template = emailTemplates[data.template] ++ const dataCC = data.cc || [] ++ const templateCC = template.cc || [] ++ const dataRecipients = data.recipients || [] ++ const templateRecipients = template.recipients || [] + const subjectBody = { + subject: data.subject || template.subject, +- body: data.body || template.body, +- }; ++ body: data.body || template.body ++ } + for (const key in subjectBody) { + subjectBody[key] = await helper.substituteStringByObject( + subjectBody[key], + data.data +- ); ++ ) + } + const emailData = { + // override template if coming data already have the 'from' address +@@ -407,9 +407,9 @@ async function sendEmail(currentUser, data) { + cc: _.uniq([...dataCC, ...templateCC]), + data: { ...data.data, ...subjectBody }, + sendgrid_template_id: template.sendgridTemplateId, +- version: 'v3', +- }; +- await helper.postEvent(config.EMAIL_TOPIC, emailData); ++ version: 'v3' ++ } ++ await helper.postEvent(config.EMAIL_TOPIC, emailData) + } + + sendEmail.schema = Joi.object() +@@ -423,11 +423,11 @@ sendEmail.schema = Joi.object() + data: Joi.object().required(), + from: Joi.string().email(), + recipients: Joi.array().items(Joi.string().email()).allow(null), +- cc: Joi.array().items(Joi.string().email()).allow(null), ++ cc: Joi.array().items(Joi.string().email()).allow(null) + }) +- .required(), ++ .required() + }) +- .required(); ++ .required() + + /** + * Add a member to a team as customer. +@@ -437,25 +437,25 @@ sendEmail.schema = Joi.object() + * @param {String} fields the fields to be returned + * @returns {Object} the member added + */ +-async function _addMemberToProjectAsCustomer(projectId, userId, fields) { ++async function _addMemberToProjectAsCustomer (projectId, userId, fields) { + try { + const member = await helper.createProjectMember( + projectId, + { userId: userId, role: 'customer' }, + { fields } +- ); +- return member; ++ ) ++ return member + } catch (err) { +- err.message = _.get(err, 'response.body.message') || err.message; ++ err.message = _.get(err, 'response.body.message') || err.message + if (err.message && err.message.includes('User already registered')) { +- throw new Error('User is already added'); ++ throw new Error('User is already added') + } + logger.error({ + component: 'TeamService', + context: '_addMemberToProjectAsCustomer', +- message: err.message, +- }); +- throw err; ++ message: err.message ++ }) ++ throw err + } + } + +@@ -467,16 +467,16 @@ async function _addMemberToProjectAsCustomer(projectId, userId, fields) { + * @param {Object} data the object including members with handle/email to be added + * @returns {Object} the success/failed added members + */ +-async function addMembers(currentUser, id, criteria, data) { +- await helper.getProjectById(currentUser, id); // check whether the user can access the project ++async function addMembers (currentUser, id, criteria, data) { ++ await helper.getProjectById(currentUser, id) // check whether the user can access the project + + const result = { + success: [], +- failed: [], +- }; ++ failed: [] ++ } + +- const handles = data.handles || []; +- const emails = data.emails || []; ++ const handles = data.handles || [] ++ const emails = data.emails || [] + + const handleMembers = await helper + .getMemberDetailsByHandles(handles) +@@ -484,9 +484,9 @@ async function addMembers(currentUser, id, criteria, data) { + _.map(members, (member) => ({ + ...member, + // populate members with lower-cased handle for case insensitive search +- handleLowerCase: member.handle.toLowerCase(), ++ handleLowerCase: member.handle.toLowerCase() + })) +- ); ++ ) + + const emailMembers = await helper + .getMemberDetailsByEmails(emails) +@@ -494,20 +494,20 @@ async function addMembers(currentUser, id, criteria, data) { + _.map(members, (member) => ({ + ...member, + // populate members with lower-cased email for case insensitive search +- emailLowerCase: member.email.toLowerCase(), ++ emailLowerCase: member.email.toLowerCase() + })) +- ); ++ ) + + await Promise.all([ + Promise.all( + handles.map((handle) => { + const memberDetails = _.find(handleMembers, { +- handleLowerCase: handle.toLowerCase(), +- }); ++ handleLowerCase: handle.toLowerCase() ++ }) + + if (!memberDetails) { +- result.failed.push({ error: "User doesn't exist", handle }); +- return; ++ result.failed.push({ error: "User doesn't exist", handle }) ++ return + } + + return _addMemberToProjectAsCustomer( +@@ -517,23 +517,23 @@ async function addMembers(currentUser, id, criteria, data) { + ) + .then((member) => { + // note, that we return `handle` in the same case it was in request +- result.success.push({ ...member, handle }); ++ result.success.push({ ...member, handle }) + }) + .catch((err) => { +- result.failed.push({ error: err.message, handle }); +- }); ++ result.failed.push({ error: err.message, handle }) ++ }) + }) + ), + + Promise.all( + emails.map((email) => { + const memberDetails = _.find(emailMembers, { +- emailLowerCase: email.toLowerCase(), +- }); ++ emailLowerCase: email.toLowerCase() ++ }) + + if (!memberDetails) { +- result.failed.push({ error: "User doesn't exist", email }); +- return; ++ result.failed.push({ error: "User doesn't exist", email }) ++ return + } + + return _addMemberToProjectAsCustomer( +@@ -543,16 +543,16 @@ async function addMembers(currentUser, id, criteria, data) { + ) + .then((member) => { + // note, that we return `email` in the same case it was in request +- result.success.push({ ...member, email }); ++ result.success.push({ ...member, email }) + }) + .catch((err) => { +- result.failed.push({ error: err.message, email }); +- }); ++ result.failed.push({ error: err.message, email }) ++ }) + }) +- ), +- ]); ++ ) ++ ]) + +- return result; ++ return result + } + + addMembers.schema = Joi.object() +@@ -561,18 +561,18 @@ addMembers.schema = Joi.object() + id: Joi.number().integer().required(), + criteria: Joi.object() + .keys({ +- fields: Joi.string(), ++ fields: Joi.string() + }) + .required(), + data: Joi.object() + .keys({ + handles: Joi.array().items(Joi.string()), +- emails: Joi.array().items(Joi.string().email()), ++ emails: Joi.array().items(Joi.string().email()) + }) + .or('handles', 'emails') +- .required(), ++ .required() + }) +- .required(); ++ .required() + + /** + * Search members in a team. +@@ -583,9 +583,9 @@ addMembers.schema = Joi.object() + * @params {Object} criteria the search criteria + * @returns {Object} the search result + */ +-async function searchMembers(currentUser, id, criteria) { +- const result = await helper.listProjectMembers(currentUser, id, criteria); +- return { result }; ++async function searchMembers (currentUser, id, criteria) { ++ const result = await helper.listProjectMembers(currentUser, id, criteria) ++ return { result } + } + + searchMembers.schema = Joi.object() +@@ -595,11 +595,11 @@ searchMembers.schema = Joi.object() + criteria: Joi.object() + .keys({ + role: Joi.string(), +- fields: Joi.string(), ++ fields: Joi.string() + }) +- .required(), ++ .required() + }) +- .required(); ++ .required() + + /** + * Search member invites for a team. +@@ -610,13 +610,13 @@ searchMembers.schema = Joi.object() + * @params {Object} criteria the search criteria + * @returns {Object} the search result + */ +-async function searchInvites(currentUser, id, criteria) { ++async function searchInvites (currentUser, id, criteria) { + const result = await helper.listProjectMemberInvites( + currentUser, + id, + criteria +- ); +- return { result }; ++ ) ++ return { result } + } + + searchInvites.schema = Joi.object() +@@ -625,11 +625,11 @@ searchInvites.schema = Joi.object() + id: Joi.number().integer().required(), + criteria: Joi.object() + .keys({ +- fields: Joi.string(), ++ fields: Joi.string() + }) +- .required(), ++ .required() + }) +- .required(); ++ .required() + + /** + * Remove a member from a team. +@@ -640,17 +640,17 @@ searchInvites.schema = Joi.object() + * @param {String} projectMemberId the id of the project member + * @returns {undefined} + */ +-async function deleteMember(currentUser, id, projectMemberId) { +- await helper.deleteProjectMember(currentUser, id, projectMemberId); ++async function deleteMember (currentUser, id, projectMemberId) { ++ await helper.deleteProjectMember(currentUser, id, projectMemberId) + } + + deleteMember.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), +- projectMemberId: Joi.number().integer().required(), ++ projectMemberId: Joi.number().integer().required() + }) +- .required(); ++ .required() + + /** + * Return details about the current user. +@@ -659,31 +659,31 @@ deleteMember.schema = Joi.object() + * @params {Object} criteria the search criteria + * @returns {Object} the user data for current user + */ +-async function getMe(currentUser) { +- return helper.getUserByExternalId(currentUser.userId); ++async function getMe (currentUser) { ++ return helper.getUserByExternalId(currentUser.userId) + } + + getMe.schema = Joi.object() + .keys({ +- currentUser: Joi.object().required(), ++ currentUser: Joi.object().required() + }) +- .required(); ++ .required() + + /** + * @param {Object} currentUser the user performing the operation. + * @param {Object} data project data + * @returns {Object} the created project + */ +-async function createProj(currentUser, data) { +- return helper.createProject(currentUser, data); ++async function createProj (currentUser, data) { ++ return helper.createProject(currentUser, data) + } + + createProj.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), +- data: Joi.object().required(), ++ data: Joi.object().required() + }) +- .required(); ++ .required() + + module.exports = { + searchTeams, +@@ -695,5 +695,5 @@ module.exports = { + searchInvites, + deleteMember, + getMe, +- createProj, +-}; ++ createProj ++} +-- +2.29.1.windows.1 + From 4d78351b8990bb4f053d003e107a8bc2a60d5faf Mon Sep 17 00:00:00 2001 From: eisbilir <72204071+eisbilir@users.noreply.github.com> Date: Mon, 31 May 2021 18:42:36 +0300 Subject: [PATCH 23/86] Delete taas-apis.patch --- taas-apis.patch | 9418 ----------------------------------------------- 1 file changed, 9418 deletions(-) delete mode 100644 taas-apis.patch diff --git a/taas-apis.patch b/taas-apis.patch deleted file mode 100644 index dcec96d8..00000000 --- a/taas-apis.patch +++ /dev/null @@ -1,9418 +0,0 @@ -From 79e73a98f25a56e793aaadbb9255bf3a99ecedd5 Mon Sep 17 00:00:00 2001 -From: eisbilir -Date: Sat, 29 May 2021 00:14:42 +0300 -Subject: [PATCH] role endpoint added - ---- - README.md | 6 +- - app-constants.js | 8 +- - config/default.js | 9 + - data/demo-data.json | 260 +- - ...coder-bookings-api.postman_collection.json | 3799 ++++++++++++++++- - docs/swagger.yaml | 476 +++ - ...topcoder-bookings.postman_environment.json | 56 +- - local/kafka-client/topics.txt | 3 + - migrations/2021-05-27-1-role-table-create.js | 146 + - .../2021-05-27-2-job-add-roleIds-field.js | 19 + - package.json | 1 + - scripts/data/exportData.js | 2 +- - scripts/data/importData.js | 2 +- - scripts/es/createIndex.js | 3 +- - scripts/es/deleteIndex.js | 3 +- - scripts/es/reIndexAll.js | 1 + - scripts/es/reIndexRoles.js | 37 + - src/bootstrap.js | 3 +- - src/common/helper.js | 1115 ++--- - src/controllers/RoleController.js | 59 + - src/controllers/TeamController.js | 62 +- - src/eventHandlers/RoleEventHandler.js | 64 + - src/eventHandlers/index.js | 4 +- - src/models/Job.js | 6 + - src/models/Role.js | 165 + - src/routes/RoleRoutes.js | 41 + - src/routes/TeamRoutes.js | 48 +- - src/services/InterviewService.js | 4 +- - src/services/JobService.js | 42 +- - src/services/ResourceBookingService.js | 1 + - src/services/RoleService.js | 305 ++ - src/services/TeamService.js | 390 +- - 32 files changed, 6271 insertions(+), 869 deletions(-) - create mode 100644 migrations/2021-05-27-1-role-table-create.js - create mode 100644 migrations/2021-05-27-2-job-add-roleIds-field.js - create mode 100644 scripts/es/reIndexRoles.js - create mode 100644 src/controllers/RoleController.js - create mode 100644 src/eventHandlers/RoleEventHandler.js - create mode 100644 src/models/Role.js - create mode 100644 src/routes/RoleRoutes.js - create mode 100644 src/services/RoleService.js - -diff --git a/README.md b/README.md -index 5e3895c..aa36c62 100644 ---- a/README.md -+++ b/README.md -@@ -87,6 +87,9 @@ - tc-taas-es-processor | [2021-04-09T21:20:19.035Z] app INFO : Starting kafka consumer - tc-taas-es-processor | 2021-04-09T21:20:21.292Z INFO no-kafka-client Joined group taas-es-processor generationId 1 as no-kafka-client-076538fc-60dd-4ca4-a2b9-520bdf73bc9e - tc-taas-es-processor | 2021-04-09T21:20:21.293Z INFO no-kafka-client Elected as group leader -+ tc-taas-es-processor | 2021-04-09T21:20:21.449Z DEBUG no-kafka-client Subscribed to taas.role.update:0 offset 0 leader kafka:9093 -+ tc-taas-es-processor | 2021-04-09T21:20:21.450Z DEBUG no-kafka-client Subscribed to taas.role.delete:0 offset 0 leader kafka:9093 -+ tc-taas-es-processor | 2021-04-09T21:20:21.451Z DEBUG no-kafka-client Subscribed to taas.role.requested:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-04-09T21:20:21.452Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.create:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-04-09T21:20:21.455Z DEBUG no-kafka-client Subscribed to taas.job.create:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-04-09T21:20:21.456Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.delete:0 offset 0 leader kafka:9093 -@@ -103,7 +106,7 @@ - tc-taas-es-processor | 2021-04-09T21:20:21.473Z DEBUG no-kafka-client Subscribed to taas.job.update:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-04-09T21:20:21.474Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.update:0 offset 0 leader kafka:9093 - tc-taas-es-processor | [2021-04-09T21:20:21.475Z] app INFO : Initialized....... -- tc-taas-es-processor | [2021-04-09T21:20:21.479Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete,taas.workperiod.create,taas.workperiod.update,taas.workperiod.delete,taas.workperiodpayment.create,taas.workperiodpayment.update,taas.workperiodpayment.delete -+ tc-taas-es-processor | [2021-04-09T21:20:21.479Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete,taas.workperiod.create,taas.workperiod.update,taas.workperiod.delete,taas.workperiodpayment.create,taas.workperiodpayment.update,taas.interview.requested,taas.interview.update,taas.interview.bulkUpdate,taas.role.requested,taas.role.update,taas.role.delete - tc-taas-es-processor | [2021-04-09T21:20:21.480Z] app INFO : Kick Start....... - tc-taas-es-processor | ********** Topcoder Health Check DropIn listening on port 3001 - tc-taas-es-processor | Topcoder Health Check DropIn started and ready to roll -@@ -194,6 +197,7 @@ To be able to change and test `taas-es-processor` locally you can follow the nex - | `npm run index:jobs ` | Indexes job data from db into ES, if jobId is not given all data is indexed. Use `-- --force` flag to skip confirmation | - | `npm run index:job-candidates ` | Indexes job candidate data from db into ES, if jobCandidateId is not given all data is indexed. Use `-- --force` flag to skip confirmation | - | `npm run index:resource-bookings ` | Indexes resource bookings data from db into ES, if resourceBookingsId is not given all data is indexed. Use `-- --force` flag to skip confirmation | -+| `npm run index:roles ` | Indexes roles data from db into ES, if roleId is not given all data is indexed. Use `-- --force` flag to skip confirmation | - | `npm run services:up` | Start services via docker-compose for local development. | - | `npm run services:down` | Stop services via docker-compose for local development. | - | `npm run services:logs -- -f ` | View logs of some service inside docker-compose. | -diff --git a/app-constants.js b/app-constants.js -index 534e46d..9b57772 100644 ---- a/app-constants.js -+++ b/app-constants.js -@@ -49,7 +49,13 @@ const Scopes = { - READ_INTERVIEW: 'read:taas-interviews', - CREATE_INTERVIEW: 'create:taas-interviews', - UPDATE_INTERVIEW: 'update:taas-interviews', -- ALL_INTERVIEW: 'all:taas-interviews' -+ ALL_INTERVIEW: 'all:taas-interviews', -+ // role -+ READ_ROLE: 'read:taas-roles', -+ CREATE_ROLE: 'create:taas-roles', -+ UPDATE_ROLE: 'update:taas-roles', -+ DELETE_ROLE: 'delete:taas-roles', -+ ALL_ROLE: 'all:taas-roles' - } - - // Interview related constants -diff --git a/config/default.js b/config/default.js -index 2b5ca7b..cf2a8a4 100644 ---- a/config/default.js -+++ b/config/default.js -@@ -76,6 +76,8 @@ module.exports = { - ES_INDEX_JOB_CANDIDATE: process.env.ES_INDEX_JOB_CANDIDATE || 'job_candidate', - // the resource booking index - ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking', -+ // the role index -+ ES_INDEX_ROLE: process.env.ES_INDEX_ROLE || 'role', - - // the max bulk size in MB for ES indexing - MAX_BULK_REQUEST_SIZE_MB: process.env.MAX_BULK_REQUEST_SIZE_MB || 20, -@@ -131,6 +133,13 @@ module.exports = { - TAAS_INTERVIEW_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_UPDATE_TOPIC || 'taas.interview.update', - // the interview bulk update Kafka message topic - TAAS_INTERVIEW_BULK_UPDATE_TOPIC: process.env.TAAS_INTERVIEW_BULK_UPDATE_TOPIC || 'taas.interview.bulkUpdate', -+ // topics for role service -+ // the create role entity Kafka message topic -+ TAAS_ROLE_CREATE_TOPIC: process.env.TAAS_ROLE_CREATE_TOPIC || 'taas.role.requested', -+ // the update role entity Kafka message topic -+ TAAS_ROLE_UPDATE_TOPIC: process.env.TAAS_ROLE_UPDATE_TOPIC || 'taas.role.update', -+ // the delete role entity Kafka message topic -+ TAAS_ROLE_DELETE_TOPIC: process.env.TAAS_ROLE_DELETE_TOPIC || 'taas.role.delete', - - // the Kafka message topic for sending email - EMAIL_TOPIC: process.env.EMAIL_TOPIC || 'external.action.email', -diff --git a/data/demo-data.json b/data/demo-data.json -index e073344..5f6c4c0 100644 ---- a/data/demo-data.json -+++ b/data/demo-data.json -@@ -20,6 +20,7 @@ - ], - "status": "in-review", - "isApplicationPageActive": false, -+ "roleIds": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-09T21:21:10.394Z", -@@ -45,6 +46,7 @@ - ], - "status": "in-review", - "isApplicationPageActive": false, -+ "roleIds": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-09T21:11:26.934Z", -@@ -70,6 +72,7 @@ - ], - "status": "in-review", - "isApplicationPageActive": false, -+ "roleIds": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-09T21:23:18.595Z", -@@ -95,6 +98,7 @@ - ], - "status": "in-review", - "isApplicationPageActive": false, -+ "roleIds": null, - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-09T21:12:09.293Z", -@@ -181,18 +185,29 @@ - "interviews": [ - { - "id": "077aa2ca-5b60-4ad9-a965-1b37e08a5046", -+ "xaiId": null, - "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", -- "googleCalendarId": null, -- "customMessage": null, -- "xaiTemplate": "interview-30", -+ "calendarEventId": null, -+ "templateUrl": "interview-30", -+ "templateId": null, -+ "templateType": null, -+ "title": null, -+ "locationDetails": null, -+ "duration": null, - "round": 1, - "startTimestamp": null, -- "attendeesList": null, -+ "endTimestamp": null, -+ "hostName": null, -+ "hostEmail": null, -+ "guestNames": null, -+ "guestEmails": null, - "status": "Completed", -+ "rescheduleUrl": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:16:10.887Z", -- "updatedAt": "2021-05-09T21:16:10.887Z" -+ "updatedAt": "2021-05-09T21:16:10.887Z", -+ "deletedAt": null - } - ] - }, -@@ -210,33 +225,55 @@ - "interviews": [ - { - "id": "b1f7ba76-640f-47e2-9463-59e51b51ec60", -+ "xaiId": null, - "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", -- "googleCalendarId": "dummyId", -- "customMessage": "This is a custom message", -- "xaiTemplate": "interview-30", -+ "calendarEventId": null, -+ "templateUrl": "interview-30", -+ "templateId": null, -+ "templateType": null, -+ "title": null, -+ "locationDetails": null, -+ "duration": null, - "round": 2, - "startTimestamp": null, -- "attendeesList": null, -+ "endTimestamp": null, -+ "hostName": null, -+ "hostEmail": null, -+ "guestNames": null, -+ "guestEmails": null, - "status": "Scheduling", -+ "rescheduleUrl": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:17:23.517Z", -- "updatedAt": "2021-05-09T21:17:23.517Z" -+ "updatedAt": "2021-05-09T21:17:23.517Z", -+ "deletedAt": null - }, - { - "id": "3144fa65-ea1a-4bec-81b0-7cb1c8845826", -+ "xaiId": null, - "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", -- "googleCalendarId": null, -- "customMessage": null, -- "xaiTemplate": "interview-30", -+ "calendarEventId": null, -+ "templateUrl": "interview-30", -+ "templateId": null, -+ "templateType": null, -+ "title": null, -+ "locationDetails": null, -+ "duration": null, - "round": 1, - "startTimestamp": null, -- "attendeesList": null, -+ "endTimestamp": null, -+ "hostName": null, -+ "hostEmail": null, -+ "guestNames": null, -+ "guestEmails": null, - "status": "Completed", -+ "rescheduleUrl": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:16:39.019Z", -- "updatedAt": "2021-05-09T21:16:39.019Z" -+ "updatedAt": "2021-05-09T21:16:39.019Z", -+ "deletedAt": null - } - ] - }, -@@ -254,54 +291,81 @@ - "interviews": [ - { - "id": "976d23a9-5710-453f-99d9-f57a588bb610", -+ "xaiId": null, - "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", -- "googleCalendarId": "dummyId", -- "customMessage": "This is a custom message", -- "xaiTemplate": "interview-30", -+ "calendarEventId": null, -+ "templateUrl": "interview-30", -+ "templateId": null, -+ "templateType": null, -+ "title": null, -+ "locationDetails": null, -+ "duration": null, - "round": 3, - "startTimestamp": null, -- "attendeesList": [ -- "attendee1@yopmail.com", -- "attendee2@yopmail.com" -- ], -+ "endTimestamp": null, -+ "hostName": null, -+ "hostEmail": null, -+ "guestNames": null, -+ "guestEmails": null, - "status": "Scheduling", -+ "rescheduleUrl": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:21:28.713Z", -- "updatedAt": "2021-05-09T21:21:28.713Z" -+ "updatedAt": "2021-05-09T21:21:28.713Z", -+ "deletedAt": null - }, - { - "id": "a23e1bf2-1084-4cfe-a0d8-d83bc6fec655", -+ "xaiId": null, - "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", -- "googleCalendarId": "dummyId", -- "customMessage": "This is a custom message", -- "xaiTemplate": "interview-30", -+ "calendarEventId": null, -+ "templateUrl": "interview-30", -+ "templateId": null, -+ "templateType": null, -+ "title": null, -+ "locationDetails": null, -+ "duration": null, - "round": 2, - "startTimestamp": null, -- "attendeesList": [ -- "attendee1@yopmail.com", -- "attendee2@yopmail.com" -- ], -+ "endTimestamp": null, -+ "hostName": null, -+ "hostEmail": null, -+ "guestNames": null, -+ "guestEmails": null, - "status": "Scheduling", -+ "rescheduleUrl": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:21:22.428Z", -- "updatedAt": "2021-05-09T21:21:22.428Z" -+ "updatedAt": "2021-05-09T21:21:22.428Z", -+ "deletedAt": null - }, - { - "id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb", -+ "xaiId": null, - "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", -- "googleCalendarId": null, -- "customMessage": null, -- "xaiTemplate": "interview-30", -+ "calendarEventId": null, -+ "templateUrl": "interview-30", -+ "templateId": null, -+ "templateType": null, -+ "title": null, -+ "locationDetails": null, -+ "duration": null, - "round": 1, - "startTimestamp": null, -- "attendeesList": null, -+ "endTimestamp": null, -+ "hostName": null, -+ "hostEmail": null, -+ "guestNames": null, -+ "guestEmails": null, - "status": "Completed", -+ "rescheduleUrl": null, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:21:17.346Z", -- "updatedAt": "2021-05-09T21:21:17.346Z" -+ "updatedAt": "2021-05-09T21:21:17.346Z", -+ "deletedAt": null - } - ] - }, -@@ -2052,5 +2116,127 @@ - } - ] - } -+ ], -+ "Role": [ -+ { -+ "id": "c145247d-5757-463d-9317-ff9e7026d403", -+ "name": "Angular Developer", -+ "description": "Angular is an open-source, client-side framework based on TypeScript and designed for building web applications.", -+ "listOfSkills": [ -+ "database", -+ "winforms", -+ "user interface (ui)", -+ "photoshop" -+ ], -+ "rates": [ -+ { -+ "global": 50, -+ "offShore": 10, -+ "inCountry": 20 -+ }, -+ { -+ "global": 25, -+ "offShore": 5, -+ "inCountry": 15 -+ } -+ ], -+ "numberOfMembers": "10", -+ "numberOfMembersAvailable": 8, -+ "imageUrl": "http://images.topcoder.com/member", -+ "timeToCandidate": 105, -+ "timeToInterview": 100, -+ "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", -+ "updatedBy": null, -+ "createdAt": "2021-05-27T21:43:08.201Z", -+ "updatedAt": "2021-05-27T21:43:08.201Z" -+ }, -+ { -+ "id": "d7ff0289-d3ea-44d8-b39a-53bba5b5b309", -+ "name": "Dev Ops Engineer", -+ "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.", -+ "listOfSkills": [ -+ "dropwizard", -+ "nginx", -+ "machine learning", -+ "force.com" -+ ], -+ "rates": [ -+ { -+ "global": 50, -+ "offShore": 10, -+ "inCountry": 20, -+ "rate20Global": 20, -+ "rate30Global": 20, -+ "rate20OffShore": 35, -+ "rate30OffShore": 35, -+ "rate20InCountry": 15, -+ "rate30InCountry": 15 -+ }, -+ { -+ "global": 25, -+ "offShore": 5, -+ "inCountry": 15, -+ "rate20Global": 20, -+ "rate30Global": 20, -+ "rate20OffShore": 35, -+ "rate30OffShore": 35, -+ "rate20InCountry": 15, -+ "rate30InCountry": 15 -+ } -+ ], -+ "numberOfMembers": "10", -+ "numberOfMembersAvailable": 8, -+ "imageUrl": "http://images.topcoder.com/member", -+ "timeToCandidate": 105, -+ "timeToInterview": 100, -+ "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", -+ "updatedBy": null, -+ "createdAt": "2021-05-27T21:43:04.717Z", -+ "updatedAt": "2021-05-27T21:43:04.717Z" -+ }, -+ { -+ "id": "e7b7e818-40d4-4102-b486-09bdd21400b8", -+ "name": "Salesforce Developer", -+ "description": "A Salesforce developer is a programmer who builds Salesforce applications across various PaaS (Platform as a Service) platforms.", -+ "listOfSkills": [ -+ "docker", -+ ".net", -+ "appcelerator", -+ "flux" -+ ], -+ "rates": [ -+ { -+ "global": 50, -+ "offShore": 10, -+ "inCountry": 20, -+ "rate20Global": 20, -+ "rate30Global": 20, -+ "rate20OffShore": 35, -+ "rate30OffShore": 35, -+ "rate20InCountry": 15, -+ "rate30InCountry": 15 -+ }, -+ { -+ "global": 25, -+ "offShore": 5, -+ "inCountry": 15, -+ "rate20Global": 20, -+ "rate30Global": 20, -+ "rate20OffShore": 35, -+ "rate30OffShore": 35, -+ "rate20InCountry": 15, -+ "rate30InCountry": 15 -+ } -+ ], -+ "numberOfMembers": "10", -+ "numberOfMembersAvailable": 6, -+ "imageUrl": "http://images.topcoder.com/member", -+ "timeToCandidate": 105, -+ "timeToInterview": 100, -+ "createdBy": "00000000-0000-0000-0000-000000000000", -+ "updatedBy": null, -+ "createdAt": "2021-05-27T21:43:09.342Z", -+ "updatedAt": "2021-05-27T21:43:09.342Z" -+ } - ] --} -+} -\ No newline at end of file -diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json -index a0518c5..96250f3 100644 ---- a/docs/Topcoder-bookings-api.postman_collection.json -+++ b/docs/Topcoder-bookings-api.postman_collection.json -@@ -1,6 +1,6 @@ - { - "info": { -- "_postman_id": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", -+ "_postman_id": "b0508e11-af20-4ea3-bfda-fec9f40ea531", - "name": "Topcoder-bookings-api", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, -@@ -17816,6 +17816,2993 @@ - } - ] - }, -+ { -+ "name": "Roles", -+ "item": [ -+ { -+ "name": "Create Role", -+ "item": [ -+ { -+ "name": "create role with admin", -+ "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(\"roleId-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 ],\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}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with booking manager", -+ "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(\"roleId-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 \"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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\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}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with m2m create", -+ "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(\"roleId-3\", response.id);\r", -+ " }\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_m2m_create_role}}" -+ } -+ ], -+ "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 ],\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\": 6,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 403', function () {\r", -+ " pm.response.to.have.status(403);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 403', function () {\r", -+ " pm.response.to.have.status(403);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role 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", -+ "type": "text", -+ "value": "Bearer invalid_token" -+ } -+ ], -+ "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with existent name", -+ "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(\"Role: \\\"Dev Ops Engineer\\\" is already exists.\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with missing parameter 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(\"\\\"role.name\\\" is required\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with missing parameter 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(\"\\\"role.rates\\\" is required\")\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 ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with missing parameter 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(\"\\\"role.rates\\\" does not contain 1 required value(s)\")\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 ],\n \"rates\": [\n\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with missing parameter 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(\"\\\"role.rates[0].global\\\" is required\")\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 ],\n \"rates\": [\n {\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with missing parameter 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(\"\\\"role.rates[0].inCountry\\\" is required\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with missing parameter 6", -+ "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(\"\\\"role.rates[0].offShore\\\" is required\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 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(\"\\\"role.name\\\" length must be less than or equal to 50 characters long\")\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 Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer 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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 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(\"\\\"role.listOfSkills[0]\\\" length must be less than or equal to 50 characters long\")\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 Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard Dropwizard\",\n \"Dropwizard\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 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(\"\\\"role.listOfSkills\\\" must be an array\")\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\":\"Dropwizard\",\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 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(\"\\\"role.rates\\\" must be an array\")\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 ],\n \"rates\": {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 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(\"\\\"role.rates[0].global\\\" must be a number\")\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 ],\n \"rates\": [\n {\n \"global\": \"first\",\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 6", -+ "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(\"\\\"role.rates[0].inCountry\\\" must be a number\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": \"fifty\",\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 7", -+ "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(\"\\\"role.numberOfMembers\\\" must be a number\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": null,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 8", -+ "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(\"\\\"role.imageUrl\\\" must be a valid uri\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 9", -+ "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(\"\\\"role.timeToCandidate\\\" must be less than or equal to 32767\")\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 99999,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "create role with invalid parameter 10", -+ "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(\"skills: \\\"teamworking,communication,problem-solving\\\" are not valid\")\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 2\",\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 \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 55,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ }, -+ { -+ "name": "Get Role", -+ "item": [ -+ { -+ "name": "get role with admin", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "get role with booking manager 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", -+ "type": "text", -+ "value": "Bearer {{token_bookingManager}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-2}}?fromDb=true", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-2}}" -+ ], -+ "query": [ -+ { -+ "key": "fromDb", -+ "value": "true" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "get role with m2m read", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_m2m_read_role}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-3}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-3}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "get role with connect user 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", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}?fromDb=true", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ], -+ "query": [ -+ { -+ "key": "fromDb", -+ "value": "true" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "get role with member", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-2}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-2}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "get role 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": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer invalid token" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-2}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-2}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "get role with invalid id", -+ "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(\"\\\"id\\\" must be a valid GUID\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/invalid", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "invalid" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "get role with missing id", -+ "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\\\" not found\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "00000000-0000-0000-0000-000000000000" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "search roles with admin", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "search roles with booking manager", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_bookingManager}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles?skillsList=dropwizard, nginx,, machine learning , FORce.com &keyword=ops e", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ], -+ "query": [ -+ { -+ "key": "skillsList", -+ "value": "dropwizard, nginx,, machine learning , FORce.com " -+ }, -+ { -+ "key": "keyword", -+ "value": "ops e" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "search roles with connect user", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles?skillsList=dataBase, ,Photoshop&keyword=sale", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ], -+ "query": [ -+ { -+ "key": "skillsList", -+ "value": "dataBase, ,Photoshop" -+ }, -+ { -+ "key": "keyword", -+ "value": "sale" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "search roles with m2m read", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_m2m_read_role}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles?skillsList=DOCKER,.NET&keyword=dev", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ], -+ "query": [ -+ { -+ "key": "skillsList", -+ "value": "DOCKER,.NET" -+ }, -+ { -+ "key": "keyword", -+ "value": "dev" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "search roles with member", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles?keyword=dev", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ], -+ "query": [ -+ { -+ "key": "keyword", -+ "value": "dev" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "search roles 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": "GET", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer invalid token" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ }, -+ { -+ "name": "Update Role", -+ "item": [ -+ { -+ "name": "update role with admin", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer edit\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with booking manager", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_bookingManager}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Angular Developer edit\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-2}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-2}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with m2m update", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_m2m_update_role}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Salesforce Developer edit\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-3}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-3}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 403', function () {\r", -+ " pm.response.to.have.status(403);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 403', function () {\r", -+ " pm.response.to.have.status(403);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role 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": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer invalid_token" -+ } -+ ], -+ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid id", -+ "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(\"\\\"id\\\" must be a valid GUID\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/invalid", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "invalid" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with missing id", -+ "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": "PATCH", -+ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "00000000-0000-0000-0000-000000000000" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with existent name", -+ "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(\"Role: \\\"Angular Developer edit\\\" is already exists.\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Angular Developer edit\"\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 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.name\\\" length must be less than or equal to 50 characters long\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer Dev Ops Engineer 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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 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.listOfSkills[0]\\\" length must be less than or equal to 50 characters long\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "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 \"Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking Teamworking\",\n \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 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.listOfSkills\\\" must be an array\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "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\":\"Teamworking\",\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 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.rates\\\" must be an array\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 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.rates[0].global\\\" must be a number\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": \"first\",\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 6", -+ "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.rates[0].inCountry\\\" must be a number\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": \"fifty\",\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 7", -+ "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.numberOfMembers\\\" must be a number\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": \"hundred\",\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 8", -+ "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.imageUrl\\\" must be a valid uri\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 9", -+ "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.timeToCandidate\\\" must be less than or equal to 32767\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "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 \"Docker\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 99999,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "update role with invalid parameter 10", -+ "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(\"skills: \\\"teamworking\\\" are not valid\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "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 \"Teamworking\",\n \".NET\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n }\n ],\n \"numberOfMembers\": 55,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 66,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ }, -+ { -+ "name": "Delete Role", -+ "item": [ -+ { -+ "name": "delete role with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 403', function () {\r", -+ " pm.response.to.have.status(403);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "delete role with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 403', function () {\r", -+ " pm.response.to.have.status(403);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "delete role 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": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer invalid_token" -+ } -+ ], -+ "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 \"teamworking\",\n \"communication\",\n \"problem-solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "delete role with invalid id", -+ "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(\"\\\"id\\\" must be a valid GUID\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/invalid", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "invalid" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "delete role with missing id", -+ "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": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "00000000-0000-0000-0000-000000000000" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "delete role with admin", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 204', function () {\r", -+ " pm.response.to.have.status(204);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "delete role with booking manager", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 204', function () {\r", -+ " pm.response.to.have.status(204);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_bookingManager}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-2}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-2}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "delete role with m2m delete", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 204', function () {\r", -+ " pm.response.to.have.status(204);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_m2m_delete_role}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-3}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-3}}" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ } -+ ] -+ }, - { - "name": "health check", - "item": [ -@@ -22399,7 +25386,227 @@ - ], - "body": { - "mode": "raw", -- "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", -+ "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_by_administrator}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "work-period-payments", -+ "{{workPeriodPaymentId_created_by_administrator}}" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ }, -+ { -+ "name": "Roles", -+ "item": [ -+ { -+ "name": "✔ create role with admin", -+ "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(\"roleId-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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ get role with admin", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ search roles with admin", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ update role with admin", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\n \"name\": \"Dev Ops Engineer edit\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ delete role with admin", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 204', function () {\r", -+ " pm.response.to.have.status(204);\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_administrator}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", - "options": { - "raw": { - "language": "json" -@@ -22407,13 +25614,13 @@ - } - }, - "url": { -- "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_by_administrator}}", -+ "raw": "{{URL}}/roles/{{roleId-1}}", - "host": [ - "{{URL}}" - ], - "path": [ -- "work-period-payments", -- "{{workPeriodPaymentId_created_by_administrator}}" -+ "roles", -+ "{{roleId-1}}" - ] - } - }, -@@ -24635,12 +27842,295 @@ - { - "key": "Authorization", - "type": "text", -- "value": "Bearer {{token_member_tester1234}}" -+ "value": "Bearer {{token_member_tester1234}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_for_member}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "work-period-payments", -+ "{{workPeriodPaymentId_created_for_member}}" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ }, -+ { -+ "name": "Roles", -+ "item": [ -+ { -+ "name": "Before Start", -+ "item": [ -+ { -+ "name": "✔ create role with admin", -+ "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(\"roleId-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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ }, -+ { -+ "name": "✘ create role with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 403', function () {\r", -+ " pm.response.to.have.status(403);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ get role with member", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ search roles with member", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles?keyword=Dev", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ], -+ "query": [ -+ { -+ "key": "keyword", -+ "value": "Dev" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✘ update role with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 403', function () {\r", -+ " pm.response.to.have.status(403);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" -+ } -+ ], -+ "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 \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✘ delete role with member", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 403', function () {\r", -+ " pm.response.to.have.status(403);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_member}}" - } - ], - "body": { - "mode": "raw", -- "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", -+ "raw": "", - "options": { - "raw": { - "language": "json" -@@ -24648,13 +28138,13 @@ - } - }, - "url": { -- "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId_created_for_member}}", -+ "raw": "{{URL}}/roles/{{roleId-1}}", - "host": [ - "{{URL}}" - ], - "path": [ -- "work-period-payments", -- "{{workPeriodPaymentId_created_for_member}}" -+ "roles", -+ "{{roleId-1}}" - ] - } - }, -@@ -26894,10 +30384,297 @@ - "response": [] - } - ] -+ }, -+ { -+ "name": "Roles", -+ "item": [ -+ { -+ "name": "Before Start", -+ "item": [ -+ { -+ "name": "✔ create role with admin", -+ "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(\"roleId-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 2\",\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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] -+ }, -+ { -+ "name": "✘ create role with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 403', function () {\r", -+ " pm.response.to.have.status(403);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "POST", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "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 ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/new", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "new" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ get role with connect user", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✔ search roles with connect user", -+ "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", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "url": { -+ "raw": "{{URL}}/roles?skillsList=Dropwizard, ,NGINX&keyword=Dev", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles" -+ ], -+ "query": [ -+ { -+ "key": "skillsList", -+ "value": "Dropwizard, ,NGINX" -+ }, -+ { -+ "key": "keyword", -+ "value": "Dev" -+ } -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✘ update role with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 403', function () {\r", -+ " pm.response.to.have.status(403);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "PATCH", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "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 \"Teamworking\",\n \"Communication\",\n \"Problem-Solving\"\n ],\n \"rates\": [\n {\n \"global\": 50,\n \"inCountry\": 20,\n \"offShore\": 10\n },\n {\n \"global\": 25,\n \"inCountry\": 15,\n \"offShore\": 5\n }\n ],\n \"numberOfMembers\": 10,\n \"imageUrl\": \"http://images.topcoder.com/member\",\n \"timeToCandidate\": 105,\n \"timeToInterview\": 100\n}", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ }, -+ { -+ "name": "✘ delete role with connect user", -+ "event": [ -+ { -+ "listen": "test", -+ "script": { -+ "exec": [ -+ "pm.test('Status code is 403', function () {\r", -+ " pm.response.to.have.status(403);\r", -+ " const response = pm.response.json()\r", -+ " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", -+ "});" -+ ], -+ "type": "text/javascript" -+ } -+ } -+ ], -+ "request": { -+ "method": "DELETE", -+ "header": [ -+ { -+ "key": "Authorization", -+ "type": "text", -+ "value": "Bearer {{token_connectUser}}" -+ } -+ ], -+ "body": { -+ "mode": "raw", -+ "raw": "", -+ "options": { -+ "raw": { -+ "language": "json" -+ } -+ } -+ }, -+ "url": { -+ "raw": "{{URL}}/roles/{{roleId-1}}", -+ "host": [ -+ "{{URL}}" -+ ], -+ "path": [ -+ "roles", -+ "{{roleId-1}}" -+ ] -+ } -+ }, -+ "response": [] -+ } -+ ] - } - ] - } - ] - } - ] --} -+} -\ No newline at end of file -diff --git a/docs/swagger.yaml b/docs/swagger.yaml -index a0b6064..e5f1ac2 100644 ---- a/docs/swagger.yaml -+++ b/docs/swagger.yaml -@@ -18,6 +18,8 @@ tags: - - name: ResourceBookings - - name: Teams - - name: WorkPeriods -+ - name: WorkPeriodPayments -+ - name: Roles - paths: - /jobs: - post: -@@ -3245,6 +3247,267 @@ paths: - application/json: - schema: - $ref: "#/components/schemas/Error" -+ /roles/new: -+ post: -+ tags: -+ - Roles -+ description: | -+ Create Role. -+ -+ **Authorization** Topcoder m2m token with create scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. -+ security: -+ - bearerAuth: [] -+ requestBody: -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/RoleRequestBody" -+ responses: -+ "200": -+ description: OK -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Role" -+ "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" -+ /roles: -+ get: -+ tags: -+ - Roles -+ description: | -+ Search roles. -+ -+ **Authorization** Topcoder m2m token with read scope is allowed. Topcoder user token with any role is allowed. -+ security: -+ - bearerAuth: [] -+ parameters: -+ - in: query -+ name: skillsList -+ required: false -+ schema: -+ type: string -+ description: comma separated skill names. case-insensitive. -+ - in: query -+ name: keyword -+ required: false -+ schema: -+ type: string -+ description: role name. case-insensitive. partial match allowed -+ responses: -+ "200": -+ description: OK -+ content: -+ application/json: -+ schema: -+ type: array -+ items: -+ $ref: "#/components/schemas/Role" -+ "400": -+ description: Bad request -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "401": -+ description: Not authenticated -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "500": -+ description: Internal Server Error -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ /roles/{id}: -+ get: -+ tags: -+ - Roles -+ description: | -+ Get role by id. -+ -+ **Authorization** Topcoder m2m token with read scope is allowed. Topcoder user token with any role is allowed. -+ security: -+ - bearerAuth: [] -+ parameters: -+ - in: path -+ name: id -+ description: The role id. -+ required: true -+ schema: -+ type: string -+ format: uuid -+ - in: query -+ name: fromDb -+ description: get data from db or not. -+ required: false -+ schema: -+ type: boolean -+ default: false -+ responses: -+ "200": -+ description: OK -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Role" -+ "400": -+ description: Bad request -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Error" -+ "401": -+ description: Not authenticated -+ 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" -+ patch: -+ tags: -+ - Roles -+ description: | -+ Partial Update role. -+ -+ **Authorization** Topcoder m2m token with update scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. -+ security: -+ - bearerAuth: [] -+ parameters: -+ - in: path -+ name: id -+ description: The id of role. -+ required: true -+ schema: -+ type: string -+ format: uuid -+ requestBody: -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/RolePatchRequestBody" -+ responses: -+ "200": -+ description: OK -+ content: -+ application/json: -+ schema: -+ $ref: "#/components/schemas/Role" -+ "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" -+ delete: -+ tags: -+ - Roles -+ description: | -+ Delete the role. -+ -+ **Authorization** Topcoder m2m token with delete scope is allowed. Topcoder user token with administrator or bookingmanager role is allowed. -+ security: -+ - bearerAuth: [] -+ parameters: -+ - in: path -+ name: id -+ description: The id of role. -+ required: true -+ schema: -+ type: string -+ format: uuid -+ responses: -+ "204": -+ description: OK -+ "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" - /health: - get: - tags: -@@ -3335,6 +3598,13 @@ components: - type: string - format: uuid - description: "The skill id." -+ roleIds: -+ type: array -+ description: "The roles." -+ items: -+ type: string -+ format: uuid -+ description: "The role id." - status: - type: string - enum: ["sourcing", "in-review", "assigned", "closed", "cancelled"] -@@ -3424,6 +3694,13 @@ components: - type: string - format: uuid - description: "The skill id." -+ roleIds: -+ type: array -+ description: "The roles." -+ items: -+ type: string -+ format: uuid -+ description: "The role id." - isApplicationPageActive: - type: boolean - default: false -@@ -3865,6 +4142,13 @@ components: - type: string - format: uuid - description: "The skill id." -+ roleIds: -+ type: array -+ description: "The roles." -+ items: -+ type: string -+ format: uuid -+ description: "The role id." - isApplicationPageActive: - type: boolean - default: false -@@ -4710,6 +4994,198 @@ components: - type: string - description: "the email of a member" - example: "xxx@xxx.com" -+ Role: -+ required: -+ - id -+ - name -+ - rates -+ - createdAt -+ - createdBy -+ properties: -+ id: -+ type: string -+ format: uuid -+ description: "The role id." -+ name: -+ type: string -+ example: "Dev Ops Engineer" -+ description: "The role name." -+ description: -+ type: string -+ example: "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." -+ description: "The role description" -+ listOfSkills: -+ type: array -+ description: "The array of skill names." -+ items: -+ type: string -+ example: "HTML" -+ description: "The skill name" -+ rates: -+ type: array -+ description: "The rates object array." -+ items: -+ $ref: "#/components/schemas/RoleRates" -+ numberOfMembers: -+ type: number -+ example: 100 -+ description: "The number of members." -+ numberOfMembersAvailable: -+ type: integer -+ example: 100 -+ description: "The number of members available." -+ imageUrl: -+ type: string -+ format: url -+ example: "http://images.topcoder.com/images" -+ description: "The image url of the role." -+ timeToCandidate: -+ type: integer -+ example: 200 -+ description: "The time to candidate." -+ timeToInterview: -+ type: integer -+ example: 300 -+ description: "The time to interview." -+ createdAt: -+ type: string -+ format: date-time -+ description: "The role created date." -+ createdBy: -+ type: string -+ format: uuid -+ description: "The user Id who created the role.(Will get the user info from the token)" -+ updatedAt: -+ type: string -+ format: date-time -+ description: "The role last updated at." -+ updatedBy: -+ type: string -+ format: uuid -+ description: "The user Id who updated the role last time.(Will get the user info from the token)" -+ RoleRequestBody: -+ required: -+ - name -+ - rates -+ properties: -+ name: -+ type: string -+ example: "Dev Ops Engineer" -+ description: "The role name." -+ description: -+ type: string -+ example: "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." -+ description: "The role description" -+ listOfSkills: -+ type: array -+ description: "The array of skill names." -+ items: -+ type: string -+ example: "HTML" -+ description: "The skill name" -+ rates: -+ type: array -+ description: "The rates object array." -+ items: -+ $ref: "#/components/schemas/RoleRates" -+ numberOfMembers: -+ type: number -+ example: 100 -+ description: "The number of members." -+ numberOfMembersAvailable: -+ type: number -+ example: 100 -+ description: "The number of members available." -+ imageUrl: -+ type: string -+ format: url -+ example: "http://images.topcoder.com/images" -+ description: "The image url of the role." -+ timeToCandidate: -+ type: integer -+ example: 200 -+ description: "The time to candidate." -+ timeToInterview: -+ type: integer -+ example: 300 -+ description: "The time to interview." -+ RolePatchRequestBody: -+ properties: -+ name: -+ type: string -+ example: "Dev Ops Engineer" -+ description: "The role name." -+ description: -+ type: string -+ example: "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." -+ description: "The role description" -+ listOfSkills: -+ type: array -+ description: "The array of skill names." -+ items: -+ type: string -+ example: "HTML" -+ description: "The skill name" -+ rates: -+ type: array -+ description: "The rates object array." -+ items: -+ $ref: "#/components/schemas/RoleRates" -+ numberOfMembers: -+ type: number -+ example: 100 -+ description: "The number of members." -+ numberOfMembersAvailable: -+ type: number -+ example: 100 -+ description: "The number of members available." -+ imageUrl: -+ type: string -+ format: url -+ example: "http://images.topcoder.com/images" -+ description: "The image url of the role." -+ timeToCandidate: -+ type: integer -+ example: 200 -+ description: "The time to candidate." -+ timeToInterview: -+ type: integer -+ example: 300 -+ description: "The time to interview." -+ RoleRates: -+ required: -+ - global -+ - inCountry -+ - offShore -+ type: object -+ properties: -+ global: -+ type: integer -+ example: 10 -+ inCountry: -+ type: integer -+ example: 20 -+ offShore: -+ type: integer -+ example: 30 -+ rate30Global: -+ type: integer -+ example: 10 -+ rate30InCountry: -+ type: integer -+ example: 20 -+ rate30OffShore: -+ type: integer -+ example: 30 -+ rate20Global: -+ type: integer -+ example: 10 -+ rate20InCountry: -+ type: integer -+ example: 20 -+ rate20OffShore: -+ type: integer -+ example: 30 - ProjectMember: - type: object - example: -diff --git a/docs/topcoder-bookings.postman_environment.json b/docs/topcoder-bookings.postman_environment.json -index 837b55d..c83fc9a 100644 ---- a/docs/topcoder-bookings.postman_environment.json -+++ b/docs/topcoder-bookings.postman_environment.json -@@ -1,5 +1,5 @@ - { -- "id": "228f4dcc-6914-462e-9b56-3285b643a2f8", -+ "id": "0ce42def-1c70-4c24-8986-914caa57f3c8", - "name": "topcoder-bookings", - "values": [ - { -@@ -312,11 +312,6 @@ - "value": "", - "enabled": true - }, -- { -- "key": "job_id_created_for_member", -- "value": "", -- "enabled": true -- }, - { - "key": "resource_bookings_id_created_for_member", - "value": "", -@@ -327,11 +322,6 @@ - "value": "", - "enabled": true - }, -- { -- "key": "job_id_created_for_connect_manager", -- "value": "", -- "enabled": true -- }, - { - "key": "resource_bookings_id_created_for_connect_manager", - "value": "", -@@ -461,9 +451,49 @@ - "key": "interview_id_created_for_connect_manager", - "value": "", - "enabled": true -+ }, -+ { -+ "key": "token_m2m_create_role", -+ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.f1QP1QTacyDxy7dwzUhBIT8blXCjKn_mnu9Cg59vIc8", -+ "enabled": true -+ }, -+ { -+ "key": "token_m2m_read_role", -+ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJyZWFkOnRhYXMtcm9sZXMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.ZeWS_W2o8YwlvIB_-z0CFFa9zhRjptCk7qNXsPPWxVY", -+ "enabled": true -+ }, -+ { -+ "key": "token_m2m_update_role", -+ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJ1cGRhdGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.0t4k0skZmxAUKuHQrG3ZrO2dgWcDMLD8W1rVluCy7XQ", -+ "enabled": true -+ }, -+ { -+ "key": "token_m2m_delete_role", -+ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJkZWxldGU6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.NSBbWOk5jCB8nIvLiZwJtR9px5wmUQaQjgpDlMDJ9hk", -+ "enabled": true -+ }, -+ { -+ "key": "token_m2m_all_role", -+ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJhbGw6dGFhcy1yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.cn0QVTOFnbHJckYqmGcpUBT8wQUxXWwtteWU7uhlDtI", -+ "enabled": true -+ }, -+ { -+ "key": "roleId-1", -+ "value": "", -+ "enabled": true -+ }, -+ { -+ "key": "roleId-2", -+ "value": "", -+ "enabled": true -+ }, -+ { -+ "key": "roleId-3", -+ "value": "", -+ "enabled": true - } - ], - "_postman_variable_scope": "environment", -- "_postman_exported_at": "2021-05-10T05:06:38.661Z", -- "_postman_exported_using": "Postman/8.3.1" -+ "_postman_exported_at": "2021-05-27T01:32:45.726Z", -+ "_postman_exported_using": "Postman/8.5.1" - } -\ No newline at end of file -diff --git a/local/kafka-client/topics.txt b/local/kafka-client/topics.txt -index 8766a1b..760c3a8 100644 ---- a/local/kafka-client/topics.txt -+++ b/local/kafka-client/topics.txt -@@ -3,16 +3,19 @@ taas.jobcandidate.create - taas.resourcebooking.create - taas.workperiod.create - taas.workperiodpayment.create -+taas.role.requested - taas.job.update - taas.jobcandidate.update - taas.resourcebooking.update - taas.workperiod.update - taas.workperiodpayment.update -+taas.role.update - taas.job.delete - taas.jobcandidate.delete - taas.resourcebooking.delete - taas.workperiod.delete - taas.workperiodpayment.delete -+taas.role.delete - taas.interview.requested - taas.interview.update - taas.interview.bulkUpdate -diff --git a/migrations/2021-05-27-1-role-table-create.js b/migrations/2021-05-27-1-role-table-create.js -new file mode 100644 -index 0000000..bce2ae1 ---- /dev/null -+++ b/migrations/2021-05-27-1-role-table-create.js -@@ -0,0 +1,146 @@ -+const config = require('config') -+ -+/* -+ * Create role table -+ */ -+ -+module.exports = { -+ up: async (queryInterface, Sequelize) => { -+ const transaction = await queryInterface.sequelize.transaction() -+ try { -+ await queryInterface.createTable('roles', { -+ id: { -+ type: Sequelize.UUID, -+ primaryKey: true, -+ allowNull: false, -+ defaultValue: Sequelize.UUIDV4 -+ }, -+ name: { -+ type: Sequelize.STRING(50), -+ allowNull: false -+ }, -+ description: { -+ type: Sequelize.STRING(1000) -+ }, -+ listOfSkills: { -+ field: 'list_of_skills', -+ type: Sequelize.ARRAY({ -+ type: Sequelize.STRING(50) -+ }) -+ }, -+ rates: { -+ type: Sequelize.ARRAY({ -+ type: Sequelize.JSONB({ -+ global: { -+ type: Sequelize.SMALLINT, -+ allowNull: false -+ }, -+ inCountry: { -+ field: 'in_country', -+ type: Sequelize.SMALLINT, -+ allowNull: false -+ }, -+ offShore: { -+ field: 'off_shore', -+ type: Sequelize.SMALLINT, -+ allowNull: false -+ }, -+ rate30Global: { -+ field: 'rate30_global', -+ type: Sequelize.SMALLINT -+ }, -+ rate30InCountry: { -+ field: 'rate30_in_country', -+ type: Sequelize.SMALLINT -+ }, -+ rate30OffShore: { -+ field: 'rate30_off_shore', -+ type: Sequelize.SMALLINT -+ }, -+ rate20Global: { -+ field: 'rate20_global', -+ type: Sequelize.SMALLINT -+ }, -+ rate20InCountry: { -+ field: 'rate20_in_country', -+ type: Sequelize.SMALLINT -+ }, -+ rate20OffShore: { -+ field: 'rate20_off_shore', -+ type: Sequelize.SMALLINT -+ } -+ }), -+ allowNull: false -+ }), -+ allowNull: false -+ }, -+ numberOfMembers: { -+ field: 'number_of_members', -+ type: Sequelize.NUMERIC -+ }, -+ numberOfMembersAvailable: { -+ field: 'number_of_members_available', -+ type: Sequelize.SMALLINT -+ }, -+ imageUrl: { -+ field: 'image_url', -+ type: Sequelize.STRING(255) -+ }, -+ timeToCandidate: { -+ field: 'time_to_candidate', -+ type: Sequelize.SMALLINT -+ }, -+ timeToInterview: { -+ field: 'time_to_interview', -+ type: Sequelize.SMALLINT -+ }, -+ 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, -+ transaction -+ }) -+ await queryInterface.addIndex( -+ { -+ tableName: 'roles', -+ schema: config.DB_SCHEMA_NAME -+ }, -+ ['name'], -+ { -+ type: 'UNIQUE', -+ where: { deleted_at: null }, -+ transaction: transaction -+ } -+ ) -+ await transaction.commit() -+ } catch (err) { -+ await transaction.rollback() -+ throw err -+ } -+ }, -+ down: async (queryInterface, Sequelize) => { -+ await queryInterface.dropTable({ -+ tableName: 'roles', -+ schema: config.DB_SCHEMA_NAME -+ }) -+ } -+} -diff --git a/migrations/2021-05-27-2-job-add-roleIds-field.js b/migrations/2021-05-27-2-job-add-roleIds-field.js -new file mode 100644 -index 0000000..a5b9f4b ---- /dev/null -+++ b/migrations/2021-05-27-2-job-add-roleIds-field.js -@@ -0,0 +1,19 @@ -+const config = require('config') -+ -+/* -+ * Add roleIds field to the Job model. -+ */ -+ -+module.exports = { -+ up: async (queryInterface, Sequelize) => { -+ await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'role_ids', -+ { -+ type: Sequelize.ARRAY({ -+ type: Sequelize.UUID -+ }) -+ }) -+ }, -+ down: async (queryInterface, Sequelize) => { -+ await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'role_ids') -+ } -+} -diff --git a/package.json b/package.json -index 0fa24cc..510504f 100644 ---- a/package.json -+++ b/package.json -@@ -15,6 +15,7 @@ - "index:jobs": "node scripts/es/reIndexJobs.js", - "index:job-candidates": "node scripts/es/reIndexJobCandidates.js", - "index:resource-bookings": "node scripts/es/reIndexResourceBookings.js", -+ "index:roles": "node scripts/es/reIndexRoles.js", - "data:export": "node scripts/data/exportData.js", - "data:import": "node scripts/data/importData.js", - "migrate": "npx sequelize db:migrate", -diff --git a/scripts/data/exportData.js b/scripts/data/exportData.js -index 4eee1ad..cb61e58 100644 ---- a/scripts/data/exportData.js -+++ b/scripts/data/exportData.js -@@ -28,7 +28,7 @@ const resourceBookingModelOpts = { - - const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH - const userPrompt = `WARNING: are you sure you want to export all data in the database to a json file with the path ${filePath}? This will overwrite the file.` --const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts] -+const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts, 'Role'] - - async function exportData () { - await helper.promptUser(userPrompt, async () => { -diff --git a/scripts/data/importData.js b/scripts/data/importData.js -index 2e9c168..a0aeeb6 100644 ---- a/scripts/data/importData.js -+++ b/scripts/data/importData.js -@@ -28,7 +28,7 @@ const resourceBookingModelOpts = { - - const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH - const userPrompt = `WARNING: this would remove existing data. Are you sure you want to import data from a json file with the path ${filePath}?` --const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts] -+const dataModels = ['Job', jobCandidateModelOpts, resourceBookingModelOpts, 'Role'] - - async function importData () { - await helper.promptUser(userPrompt, async () => { -diff --git a/scripts/es/createIndex.js b/scripts/es/createIndex.js -index d2c7294..269cd5a 100644 ---- a/scripts/es/createIndex.js -+++ b/scripts/es/createIndex.js -@@ -8,7 +8,8 @@ const helper = require('../../src/common/helper') - const indices = [ - config.get('esConfig.ES_INDEX_JOB'), - config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), -- config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') -+ config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), -+ config.get('esConfig.ES_INDEX_ROLE') - ] - const userPrompt = `WARNING: Are you sure want to create the following elasticsearch indices: ${indices}?` - -diff --git a/scripts/es/deleteIndex.js b/scripts/es/deleteIndex.js -index 6e30995..724d355 100644 ---- a/scripts/es/deleteIndex.js -+++ b/scripts/es/deleteIndex.js -@@ -8,7 +8,8 @@ const helper = require('../../src/common/helper') - const indices = [ - config.get('esConfig.ES_INDEX_JOB'), - config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), -- config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') -+ config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), -+ config.get('esConfig.ES_INDEX_ROLE') - ] - const userPrompt = `WARNING: this would remove existent data! Are you sure want to delete the following eleasticsearch indices: ${indices}?` - -diff --git a/scripts/es/reIndexAll.js b/scripts/es/reIndexAll.js -index 802695d..0367be1 100644 ---- a/scripts/es/reIndexAll.js -+++ b/scripts/es/reIndexAll.js -@@ -34,6 +34,7 @@ async function indexAll () { - await helper.indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) - await helper.indexBulkDataToES(jobCandidateModelOpts, config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger) - await helper.indexBulkDataToES(resourceBookingModelOpts, config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger) -+ await helper.indexBulkDataToES('Role', config.get('esConfig.ES_INDEX_ROLE'), logger) - process.exit(0) - } catch (err) { - logger.logFullError(err, { component: 'indexAll' }) -diff --git a/scripts/es/reIndexRoles.js b/scripts/es/reIndexRoles.js -new file mode 100644 -index 0000000..a4507aa ---- /dev/null -+++ b/scripts/es/reIndexRoles.js -@@ -0,0 +1,37 @@ -+/** -+ * Reindex Roles data in Elasticsearch using data from database -+ */ -+const config = require('config') -+const logger = require('../../src/common/logger') -+const helper = require('../../src/common/helper') -+ -+const roleId = helper.getParamFromCliArgs() -+const index = config.get('esConfig.ES_INDEX_ROLE') -+const reIndexAllRolesPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the index ${index}?` -+const reIndexRolePrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the document with id ${roleId} in index ${index}?` -+ -+async function reIndexRoles () { -+ if (roleId === null) { -+ await helper.promptUser(reIndexAllRolesPrompt, async () => { -+ try { -+ await helper.indexBulkDataToES('Role', index, logger) -+ process.exit(0) -+ } catch (err) { -+ logger.logFullError(err, { component: 'reIndexRoles' }) -+ process.exit(1) -+ } -+ }) -+ } else { -+ await helper.promptUser(reIndexRolePrompt, async () => { -+ try { -+ await helper.indexDataToEsById(roleId, 'Role', index, logger) -+ process.exit(0) -+ } catch (err) { -+ logger.logFullError(err, { component: 'reIndexRoles' }) -+ process.exit(1) -+ } -+ }) -+ } -+} -+ -+reIndexRoles() -diff --git a/src/bootstrap.js b/src/bootstrap.js -index 2999f13..896e6c9 100644 ---- a/src/bootstrap.js -+++ b/src/bootstrap.js -@@ -16,7 +16,7 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') - Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') - Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') - Joi.workload = () => Joi.string().valid('full-time', 'fractional') --Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed') -+Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed') - Joi.title = () => Joi.string().max(128) - Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') - Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) -@@ -26,6 +26,7 @@ Joi.workPeriodPaymentStatus = () => Joi.string().valid('completed', 'cancelled') - // See https://joi.dev/api/?v=17.3.0#string fro details why it's like this. - // In many cases we would like to allow empty string to make it easier to create UI for editing data. - Joi.stringAllowEmpty = () => Joi.string().allow('') -+Joi.smallint = () => Joi.number().min(-32768).max(32767) - - function buildServices (dir) { - const files = fs.readdirSync(dir) -diff --git a/src/common/helper.js b/src/common/helper.js -index 0ce1190..66cf32d 100644 ---- a/src/common/helper.js -+++ b/src/common/helper.js -@@ -2,50 +2,50 @@ - * This file defines helper methods - */ - --const fs = require('fs'); --const querystring = require('querystring'); --const Confirm = require('prompt-confirm'); --const Bottleneck = require('bottleneck'); --const AWS = require('aws-sdk'); --const config = require('config'); --const HttpStatus = require('http-status-codes'); --const _ = require('lodash'); --const request = require('superagent'); --const elasticsearch = require('@elastic/elasticsearch'); -+const fs = require('fs') -+const querystring = require('querystring') -+const Confirm = require('prompt-confirm') -+const Bottleneck = require('bottleneck') -+const AWS = require('aws-sdk') -+const config = require('config') -+const HttpStatus = require('http-status-codes') -+const _ = require('lodash') -+const request = require('superagent') -+const elasticsearch = require('@elastic/elasticsearch') - const { -- ResponseError: ESResponseError, --} = require('@elastic/elasticsearch/lib/errors'); --const errors = require('../common/errors'); --const logger = require('./logger'); --const models = require('../models'); --const eventDispatcher = require('./eventDispatcher'); --const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper'); --const moment = require('moment'); -+ ResponseError: ESResponseError -+} = require('@elastic/elasticsearch/lib/errors') -+const errors = require('../common/errors') -+const logger = require('./logger') -+const models = require('../models') -+const eventDispatcher = require('./eventDispatcher') -+const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') -+const moment = require('moment') - - const localLogger = { - debug: (message) => - logger.debug({ - component: 'helper', - context: message.context, -- message: message.message, -+ message: message.message - }), - error: (message) => - logger.error({ - component: 'helper', - context: message.context, -- message: message.message, -+ message: message.message - }), - info: (message) => - logger.info({ - component: 'helper', - context: message.context, -- message: message.message, -- }), --}; -+ message: message.message -+ }) -+} - --AWS.config.region = config.esConfig.AWS_REGION; -+AWS.config.region = config.esConfig.AWS_REGION - --const m2mAuth = require('tc-core-library-js').auth.m2m; -+const m2mAuth = require('tc-core-library-js').auth.m2m - - const m2m = m2mAuth( - _.pick(config, [ -@@ -53,9 +53,9 @@ const m2m = m2mAuth( - 'AUTH0_AUDIENCE', - 'AUTH0_CLIENT_ID', - 'AUTH0_CLIENT_SECRET', -- 'AUTH0_PROXY_SERVER_URL', -+ 'AUTH0_PROXY_SERVER_URL' - ]) --); -+) - - const m2mForUbahn = m2mAuth({ - AUTH0_AUDIENCE: config.AUTH0_AUDIENCE_UBAHN, -@@ -64,20 +64,20 @@ const m2mForUbahn = m2mAuth({ - 'TOKEN_CACHE_TIME', - 'AUTH0_CLIENT_ID', - 'AUTH0_CLIENT_SECRET', -- 'AUTH0_PROXY_SERVER_URL', -- ]), --}); -+ 'AUTH0_PROXY_SERVER_URL' -+ ]) -+}) - --let busApiClient; -+let busApiClient - - /** - * Get bus api client. - * - * @returns {Object} the bus api client - */ --function getBusApiClient() { -+function getBusApiClient () { - if (busApiClient) { -- return busApiClient; -+ return busApiClient - } - busApiClient = busApi( - _.pick(config, [ -@@ -88,17 +88,17 @@ function getBusApiClient() { - 'AUTH0_CLIENT_SECRET', - 'BUSAPI_URL', - 'KAFKA_ERROR_TOPIC', -- 'AUTH0_PROXY_SERVER_URL', -+ 'AUTH0_PROXY_SERVER_URL' - ]) -- ); -- return busApiClient; -+ ) -+ return busApiClient - } - - // ES Client mapping --const esClients = {}; -+const esClients = {} - - // The es index property mapping --const esIndexPropertyMapping = {}; -+const esIndexPropertyMapping = {} - esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { - projectId: { type: 'integer' }, - externalId: { type: 'keyword' }, -@@ -113,11 +113,12 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { - skills: { type: 'keyword' }, - status: { type: 'keyword' }, - isApplicationPageActive: { type: 'boolean' }, -+ roleIds: { type: 'keyword' }, - createdAt: { type: 'date' }, - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, -- updatedBy: { type: 'keyword' }, --}; -+ updatedBy: { type: 'keyword' } -+} - esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { - jobId: { type: 'keyword' }, - userId: { type: 'keyword' }, -@@ -150,14 +151,14 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' }, -- deletedAt: { type: 'date' }, -- }, -+ deletedAt: { type: 'date' } -+ } - }, - createdAt: { type: 'date' }, - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, -- updatedBy: { type: 'keyword' }, --}; -+ updatedBy: { type: 'keyword' } -+} - esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { - projectId: { type: 'integer' }, - userId: { type: 'keyword' }, -@@ -195,32 +196,59 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { - createdAt: { type: 'date' }, - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, -- updatedBy: { type: 'keyword' }, -- }, -+ updatedBy: { type: 'keyword' } -+ } - }, - createdAt: { type: 'date' }, - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, -- updatedBy: { type: 'keyword' }, -- }, -+ updatedBy: { type: 'keyword' } -+ } -+ }, -+ createdAt: { type: 'date' }, -+ createdBy: { type: 'keyword' }, -+ updatedAt: { type: 'date' }, -+ updatedBy: { type: 'keyword' } -+} -+esIndexPropertyMapping[config.get('esConfig.ES_INDEX_ROLE')] = { -+ name: { type: 'keyword' }, -+ description: { type: 'keyword' }, -+ listOfSkills: { type: 'keyword' }, -+ rates: { -+ properties: { -+ global: { type: 'integer' }, -+ inCountry: { type: 'integer' }, -+ offShore: { type: 'integer' }, -+ rate30Global: { type: 'integer' }, -+ rate30InCountry: { type: 'integer' }, -+ rate30OffShore: { type: 'integer' }, -+ rate20Global: { type: 'integer' }, -+ rate20InCountry: { type: 'integer' }, -+ rate20OffShore: { type: 'integer' } -+ } - }, -+ numberOfMembers: { type: 'integer' }, -+ numberOfMembersAvailable: { type: 'integer' }, -+ imageUrl: { type: 'keyword' }, -+ timeToCandidate: { type: 'integer' }, -+ timeToInterview: { type: 'integer' }, - createdAt: { type: 'date' }, - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, -- updatedBy: { type: 'keyword' }, --}; -+ updatedBy: { type: 'keyword' } -+} - - /** - * Get the first parameter from cli arguments - */ --function getParamFromCliArgs() { -- const filteredArgs = process.argv.filter((arg) => !arg.includes('--')); -+function getParamFromCliArgs () { -+ const filteredArgs = process.argv.filter((arg) => !arg.includes('--')) - - if (filteredArgs.length > 2) { -- return filteredArgs[2]; -+ return filteredArgs[2] - } - -- return null; -+ return null - } - - /** -@@ -228,18 +256,18 @@ function getParamFromCliArgs() { - * @param {string} promptQuery the query to ask the user - * @param {function} cb the callback function - */ --async function promptUser(promptQuery, cb) { -+async function promptUser (promptQuery, cb) { - if (process.argv.includes('--force')) { -- await cb(); -- return; -+ await cb() -+ return - } - -- const prompt = new Confirm(promptQuery); -+ const prompt = new Confirm(promptQuery) - prompt.ask(async (answer) => { - if (answer) { -- await cb(); -+ await cb() - } -- }); -+ }) - } - - /** -@@ -248,23 +276,23 @@ async function promptUser(promptQuery, cb) { - * @param {Object} logger the logger object - * @param {Object} esClient the elasticsearch client (optional, will create if not given) - */ --async function createIndex(index, logger, esClient = null) { -+async function createIndex (index, logger, esClient = null) { - if (!esClient) { -- esClient = getESClient(); -+ esClient = getESClient() - } - - await esClient.indices.create({ - index, - body: { - mappings: { -- properties: esIndexPropertyMapping[index], -- }, -- }, -- }); -+ properties: esIndexPropertyMapping[index] -+ } -+ } -+ }) - logger.info({ - component: 'createIndex', -- message: `ES Index ${index} creation succeeded!`, -- }); -+ message: `ES Index ${index} creation succeeded!` -+ }) - } - - /** -@@ -273,45 +301,45 @@ async function createIndex(index, logger, esClient = null) { - * @param {Object} logger the logger object - * @param {Object} esClient the elasticsearch client (optional, will create if not given) - */ --async function deleteIndex(index, logger, esClient = null) { -+async function deleteIndex (index, logger, esClient = null) { - if (!esClient) { -- esClient = getESClient(); -+ esClient = getESClient() - } - -- await esClient.indices.delete({ index }); -+ await esClient.indices.delete({ index }) - logger.info({ - component: 'deleteIndex', -- message: `ES Index ${index} deletion succeeded!`, -- }); -+ message: `ES Index ${index} deletion succeeded!` -+ }) - } - - /** - * Split data into bulks - * @param {Array} data the array of data to split - */ --function getBulksFromDocuments(data) { -- const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6; -- const bulks = []; -- let documentIndex = 0; -- let currentBulkSize = 0; -- let currentBulk = []; -+function getBulksFromDocuments (data) { -+ const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6 -+ const bulks = [] -+ let documentIndex = 0 -+ let currentBulkSize = 0 -+ let currentBulk = [] - - while (true) { - // break loop when parsed all documents - if (documentIndex >= data.length) { -- bulks.push(currentBulk); -- break; -+ bulks.push(currentBulk) -+ break - } - - // check if current document size is greater than the max bulk size, if so, throw error - const currentDocumentSize = Buffer.byteLength( - JSON.stringify(data[documentIndex]), - 'utf-8' -- ); -+ ) - if (maxBytes < currentDocumentSize) { - throw new Error( - `Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.` -- ); -+ ) - } - - if ( -@@ -320,17 +348,17 @@ function getBulksFromDocuments(data) { - ) { - // if adding the current document goes over the max bulk size OR goes over max number of docs - // then push the current bulk to bulks array and reset the current bulk -- bulks.push(currentBulk); -- currentBulk = []; -- currentBulkSize = 0; -+ bulks.push(currentBulk) -+ currentBulk = [] -+ currentBulkSize = 0 - } else { - // otherwise, add document to current bulk -- currentBulk.push(data[documentIndex]); -- currentBulkSize += currentDocumentSize; -- documentIndex++; -+ currentBulk.push(data[documentIndex]) -+ currentBulkSize += currentDocumentSize -+ documentIndex++ - } - } -- return bulks; -+ return bulks - } - - /** -@@ -339,57 +367,57 @@ function getBulksFromDocuments(data) { - * @param {Object} indexName the index name - * @param {Object} logger the logger object - */ --async function indexBulkDataToES(modelOpts, indexName, logger) { -- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; -- const include = _.get(modelOpts, 'include', []); -+async function indexBulkDataToES (modelOpts, indexName, logger) { -+ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName -+ const include = _.get(modelOpts, 'include', []) - - logger.info({ - component: 'indexBulkDataToES', -- message: `Reindexing of ${modelName}s started!`, -- }); -+ message: `Reindexing of ${modelName}s started!` -+ }) - -- const esClient = getESClient(); -+ const esClient = getESClient() - - // clear index -- const indexExistsRes = await esClient.indices.exists({ index: indexName }); -+ const indexExistsRes = await esClient.indices.exists({ index: indexName }) - if (indexExistsRes.statusCode !== 404) { -- await deleteIndex(indexName, logger, esClient); -+ await deleteIndex(indexName, logger, esClient) - } -- await createIndex(indexName, logger, esClient); -+ await createIndex(indexName, logger, esClient) - - // get data from db - logger.info({ - component: 'indexBulkDataToES', -- message: 'Getting data from database', -- }); -- const model = models[modelName]; -- const data = await model.findAll({ include }); -- const rawObjects = _.map(data, (r) => r.toJSON()); -+ message: 'Getting data from database' -+ }) -+ const model = models[modelName] -+ const data = await model.findAll({ include }) -+ const rawObjects = _.map(data, (r) => r.toJSON()) - if (_.isEmpty(rawObjects)) { - logger.info({ - component: 'indexBulkDataToES', -- message: `No data in database for ${modelName}`, -- }); -- return; -+ message: `No data in database for ${modelName}` -+ }) -+ return - } -- const bulks = getBulksFromDocuments(rawObjects); -+ const bulks = getBulksFromDocuments(rawObjects) - -- const startTime = Date.now(); -- let doneCount = 0; -+ const startTime = Date.now() -+ let doneCount = 0 - for (const bulk of bulks) { - // send bulk to esclient - const body = bulk.flatMap((doc) => [ - { index: { _index: indexName, _id: doc.id } }, -- doc, -- ]); -- await esClient.bulk({ refresh: true, body }); -- doneCount += bulk.length; -+ doc -+ ]) -+ await esClient.bulk({ refresh: true, body }) -+ doneCount += bulk.length - - // log metrics -- const timeSpent = Date.now() - startTime; -- const avgTimePerDocument = timeSpent / doneCount; -- const estimatedLength = avgTimePerDocument * data.length; -- const timeLeft = startTime + estimatedLength - Date.now(); -+ const timeSpent = Date.now() - startTime -+ const avgTimePerDocument = timeSpent / doneCount -+ const estimatedLength = avgTimePerDocument * data.length -+ const timeLeft = startTime + estimatedLength - Date.now() - logger.info({ - component: 'indexBulkDataToES', - message: `Processed ${doneCount} of ${ -@@ -398,8 +426,8 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { - avgTimePerDocument - )}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime( - timeLeft -- )}`, -- }); -+ )}` -+ }) - } - } - -@@ -410,36 +438,36 @@ async function indexBulkDataToES(modelOpts, indexName, logger) { - * @param {string} id the job id - * @param {Object} logger the logger object - */ --async function indexDataToEsById(id, modelOpts, indexName, logger) { -- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; -- const include = _.get(modelOpts, 'include', []); -+async function indexDataToEsById (id, modelOpts, indexName, logger) { -+ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName -+ const include = _.get(modelOpts, 'include', []) - - logger.info({ - component: 'indexDataToEsById', -- message: `Reindexing of ${modelName} with id ${id} started!`, -- }); -- const esClient = getESClient(); -+ message: `Reindexing of ${modelName} with id ${id} started!` -+ }) -+ const esClient = getESClient() - - logger.info({ - component: 'indexDataToEsById', -- message: 'Getting data from database', -- }); -- const model = models[modelName]; -+ message: 'Getting data from database' -+ }) -+ const model = models[modelName] - -- const data = await model.findById(id, include); -+ const data = await model.findById(id, include) - logger.info({ - component: 'indexDataToEsById', -- message: 'Indexing data into Elasticsearch', -- }); -+ message: 'Indexing data into Elasticsearch' -+ }) - await esClient.index({ - index: indexName, - id: id, -- body: data.dataValues, -- }); -+ body: data.dataValues -+ }) - logger.info({ - component: 'indexDataToEsById', -- message: 'Indexing complete!', -- }); -+ message: 'Indexing complete!' -+ }) - } - - /** -@@ -448,68 +476,68 @@ async function indexDataToEsById(id, modelOpts, indexName, logger) { - * @param {Array} dataModels the data models to import - * @param {Object} logger the logger object - */ --async function importData(pathToFile, dataModels, logger) { -+async function importData (pathToFile, dataModels, logger) { - // check if file exists - if (!fs.existsSync(pathToFile)) { -- throw new Error(`File with path ${pathToFile} does not exist`); -+ throw new Error(`File with path ${pathToFile} does not exist`) - } - - // clear database -- logger.info({ component: 'importData', message: 'Clearing database...' }); -- await models.sequelize.sync({ force: true }); -+ logger.info({ component: 'importData', message: 'Clearing database...' }) -+ await models.sequelize.sync({ force: true }) - -- let transaction = null; -- let currentModelName = null; -+ let transaction = null -+ let currentModelName = null - try { - // Start a transaction -- transaction = await models.sequelize.transaction(); -- const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()); -+ transaction = await models.sequelize.transaction() -+ const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()) - - for (let index = 0; index < dataModels.length; index += 1) { -- const modelOpts = dataModels[index]; -- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; -- const include = _.get(modelOpts, 'include', []); -+ const modelOpts = dataModels[index] -+ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName -+ const include = _.get(modelOpts, 'include', []) - -- currentModelName = modelName; -- const model = models[modelName]; -- const modelRecords = jsonData[modelName]; -+ currentModelName = modelName -+ const model = models[modelName] -+ const modelRecords = jsonData[modelName] - - if (modelRecords && modelRecords.length > 0) { - logger.info({ - component: 'importData', -- message: `Importing data for model: ${modelName}`, -- }); -+ message: `Importing data for model: ${modelName}` -+ }) - -- await model.bulkCreate(modelRecords, { include, transaction }); -+ await model.bulkCreate(modelRecords, { include, transaction }) - logger.info({ - component: 'importData', -- message: `Records imported for model: ${modelName} = ${modelRecords.length}`, -- }); -+ message: `Records imported for model: ${modelName} = ${modelRecords.length}` -+ }) - } else { - logger.info({ - component: 'importData', -- message: `No records to import for model: ${modelName}`, -- }); -+ message: `No records to import for model: ${modelName}` -+ }) - } - } - // commit transaction only if all things went ok - logger.info({ - component: 'importData', -- message: 'committing transaction to database...', -- }); -- await transaction.commit(); -+ message: 'committing transaction to database...' -+ }) -+ await transaction.commit() - } catch (error) { - logger.error({ - component: 'importData', -- message: `Error while writing data of model: ${currentModelName}`, -- }); -+ message: `Error while writing data of model: ${currentModelName}` -+ }) - // rollback all insert operations - if (transaction) { - logger.info({ - component: 'importData', -- message: 'rollback database transaction...', -- }); -- transaction.rollback(); -+ message: 'rollback database transaction...' -+ }) -+ transaction.rollback() - } - if (error.name && error.errors && error.fields) { - // For sequelize validation errors, we throw only fields with data that helps in debugging error, -@@ -519,11 +547,11 @@ async function importData(pathToFile, dataModels, logger) { - modelName: currentModelName, - name: error.name, - errors: error.errors, -- fields: error.fields, -+ fields: error.fields - }) -- ); -+ ) - } else { -- throw error; -+ throw error - } - } - -@@ -533,10 +561,10 @@ async function importData(pathToFile, dataModels, logger) { - include: [ - { - model: models.Interview, -- as: 'interviews', -- }, -- ], -- }; -+ as: 'interviews' -+ } -+ ] -+ } - const resourceBookingModelOpts = { - modelName: 'ResourceBooking', - include: [ -@@ -546,23 +574,24 @@ async function importData(pathToFile, dataModels, logger) { - include: [ - { - model: models.WorkPeriodPayment, -- as: 'payments', -- }, -- ], -- }, -- ], -- }; -- await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger); -+ as: 'payments' -+ } -+ ] -+ } -+ ] -+ } -+ await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) - await indexBulkDataToES( - jobCandidateModelOpts, - config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - logger -- ); -+ ) - await indexBulkDataToES( - resourceBookingModelOpts, - config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), - logger -- ); -+ ) -+ await indexBulkDataToES('Role', config.get('esConfig.ES_INDEX_ROLE'), logger) - } - - /** -@@ -571,74 +600,74 @@ async function importData(pathToFile, dataModels, logger) { - * @param {Array} dataModels the data models to export - * @param {Object} logger the logger object - */ --async function exportData(pathToFile, dataModels, logger) { -+async function exportData (pathToFile, dataModels, logger) { - logger.info({ - component: 'exportData', -- message: `Start Saving data to file with path ${pathToFile}....`, -- }); -+ message: `Start Saving data to file with path ${pathToFile}....` -+ }) - -- const allModelsRecords = {}; -+ const allModelsRecords = {} - for (let index = 0; index < dataModels.length; index += 1) { -- const modelOpts = dataModels[index]; -- const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName; -- const include = _.get(modelOpts, 'include', []); -- const modelRecords = await models[modelName].findAll({ include }); -- const rawRecords = _.map(modelRecords, (r) => r.toJSON()); -- allModelsRecords[modelName] = rawRecords; -+ const modelOpts = dataModels[index] -+ const modelName = _.isString(modelOpts) ? modelOpts : modelOpts.modelName -+ const include = _.get(modelOpts, 'include', []) -+ const modelRecords = await models[modelName].findAll({ include }) -+ const rawRecords = _.map(modelRecords, (r) => r.toJSON()) -+ allModelsRecords[modelName] = rawRecords - logger.info({ - component: 'exportData', -- message: `Records loaded for model: ${modelName} = ${rawRecords.length}`, -- }); -+ message: `Records loaded for model: ${modelName} = ${rawRecords.length}` -+ }) - } - -- fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)); -+ fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)) - logger.info({ - component: 'exportData', -- message: 'End Saving data to file....', -- }); -+ message: 'End Saving data to file....' -+ }) - } - - /** - * Format a time in milliseconds into a human readable format - * @param {Date} milliseconds the number of milliseconds - */ --function formatTime(millisec) { -- const ms = Math.floor(millisec % 1000); -- const secs = Math.floor((millisec / 1000) % 60); -- const mins = Math.floor((millisec / (1000 * 60)) % 60); -- const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24); -- const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7); -- const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4); -- const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12); -- const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)); -- -- let formattedTime = '0 milliseconds'; -+function formatTime (millisec) { -+ const ms = Math.floor(millisec % 1000) -+ const secs = Math.floor((millisec / 1000) % 60) -+ const mins = Math.floor((millisec / (1000 * 60)) % 60) -+ const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24) -+ const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7) -+ const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4) -+ const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12) -+ const yrs = Math.floor(millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12)) -+ -+ let formattedTime = '0 milliseconds' - if (ms > 0) { -- formattedTime = `${ms} milliseconds`; -+ formattedTime = `${ms} milliseconds` - } - if (secs > 0) { -- formattedTime = `${secs} seconds ${formattedTime}`; -+ formattedTime = `${secs} seconds ${formattedTime}` - } - if (mins > 0) { -- formattedTime = `${mins} minutes ${formattedTime}`; -+ formattedTime = `${mins} minutes ${formattedTime}` - } - if (hrs > 0) { -- formattedTime = `${hrs} hours ${formattedTime}`; -+ formattedTime = `${hrs} hours ${formattedTime}` - } - if (days > 0) { -- formattedTime = `${days} days ${formattedTime}`; -+ formattedTime = `${days} days ${formattedTime}` - } - if (weeks > 0) { -- formattedTime = `${weeks} weeks ${formattedTime}`; -+ formattedTime = `${weeks} weeks ${formattedTime}` - } - if (mnths > 0) { -- formattedTime = `${mnths} months ${formattedTime}`; -+ formattedTime = `${mnths} months ${formattedTime}` - } - if (yrs > 0) { -- formattedTime = `${yrs} years ${formattedTime}`; -+ formattedTime = `${yrs} years ${formattedTime}` - } - -- return formattedTime.trim(); -+ return formattedTime.trim() - } - - /** -@@ -647,30 +676,30 @@ function formatTime(millisec) { - * @param {Array} source the array in which to search for the term - * @param {Array | String} term the term to search - */ --function checkIfExists(source, term) { -- let terms; -+function checkIfExists (source, term) { -+ let terms - - if (!_.isArray(source)) { -- throw new Error('Source argument should be an array'); -+ throw new Error('Source argument should be an array') - } - -- source = source.map((s) => s.toLowerCase()); -+ source = source.map((s) => s.toLowerCase()) - - if (_.isString(term)) { -- terms = term.toLowerCase().split(' '); -+ terms = term.toLowerCase().split(' ') - } else if (_.isArray(term)) { -- terms = term.map((t) => t.toLowerCase()); -+ terms = term.map((t) => t.toLowerCase()) - } else { -- throw new Error('Term argument should be either a string or an array'); -+ throw new Error('Term argument should be either a string or an array') - } - - for (let i = 0; i < terms.length; i++) { - if (source.includes(terms[i])) { -- return true; -+ return true - } - } - -- return false; -+ return false - } - - /** -@@ -678,10 +707,10 @@ function checkIfExists(source, term) { - * @param {Function} fn the async function - * @returns {Function} the wrapped function - */ --function wrapExpress(fn) { -+function wrapExpress (fn) { - return function (req, res, next) { -- fn(req, res, next).catch(next); -- }; -+ fn(req, res, next).catch(next) -+ } - } - - /** -@@ -689,20 +718,20 @@ function wrapExpress(fn) { - * @param obj the object (controller exports) - * @returns {Object|Array} the wrapped object - */ --function autoWrapExpress(obj) { -+function autoWrapExpress (obj) { - if (_.isArray(obj)) { -- return obj.map(autoWrapExpress); -+ return obj.map(autoWrapExpress) - } - if (_.isFunction(obj)) { - if (obj.constructor.name === 'AsyncFunction') { -- return wrapExpress(obj); -+ return wrapExpress(obj) - } -- return obj; -+ return obj - } - _.each(obj, (value, key) => { -- obj[key] = autoWrapExpress(value); -- }); -- return obj; -+ obj[key] = autoWrapExpress(value) -+ }) -+ return obj - } - - /** -@@ -711,11 +740,11 @@ function autoWrapExpress(obj) { - * @param {Number} page the page number - * @returns {String} link for the page - */ --function getPageLink(req, page) { -- const q = _.assignIn({}, req.query, { page }); -+function getPageLink (req, page) { -+ const q = _.assignIn({}, req.query, { page }) - return `${req.protocol}://${req.get('Host')}${req.baseUrl}${ - req.path -- }?${querystring.stringify(q)}`; -+ }?${querystring.stringify(q)}` - } - - /** -@@ -724,31 +753,31 @@ function getPageLink(req, page) { - * @param {Object} res the HTTP response - * @param {Object} result the operation result - */ --function setResHeaders(req, res, result) { -- const totalPages = Math.ceil(result.total / result.perPage); -+function setResHeaders (req, res, result) { -+ const totalPages = Math.ceil(result.total / result.perPage) - if (result.page > 1) { -- res.set('X-Prev-Page', result.page - 1); -+ res.set('X-Prev-Page', result.page - 1) - } - if (result.page < totalPages) { -- res.set('X-Next-Page', result.page + 1); -+ res.set('X-Next-Page', result.page + 1) - } -- res.set('X-Page', result.page); -- res.set('X-Per-Page', result.perPage); -- res.set('X-Total', result.total); -- res.set('X-Total-Pages', totalPages); -+ res.set('X-Page', result.page) -+ res.set('X-Per-Page', result.perPage) -+ res.set('X-Total', result.total) -+ res.set('X-Total-Pages', totalPages) - // set Link header - if (totalPages > 0) { - let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink( - req, - totalPages -- )}>; rel="last"`; -+ )}>; rel="last"` - if (result.page > 1) { -- link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"`; -+ link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"` - } - if (result.page < totalPages) { -- link += `, <${getPageLink(req, result.page + 1)}>; rel="next"`; -+ link += `, <${getPageLink(req, result.page + 1)}>; rel="next"` - } -- res.set('Link', link); -+ res.set('Link', link) - } - } - -@@ -756,30 +785,30 @@ function setResHeaders(req, res, result) { - * Get ES Client - * @return {Object} Elastic Host Client Instance - */ --function getESClient() { -+function getESClient () { - if (esClients.client) { -- return esClients.client; -+ return esClients.client - } - -- const host = config.esConfig.HOST; -- const cloudId = config.esConfig.ELASTICCLOUD.id; -+ const host = config.esConfig.HOST -+ const cloudId = config.esConfig.ELASTICCLOUD.id - if (cloudId) { - // Elastic Cloud configuration - esClients.client = new elasticsearch.Client({ - cloud: { -- id: cloudId, -+ id: cloudId - }, - auth: { - username: config.esConfig.ELASTICCLOUD.username, -- password: config.esConfig.ELASTICCLOUD.password, -- }, -- }); -+ password: config.esConfig.ELASTICCLOUD.password -+ } -+ }) - } else { - esClients.client = new elasticsearch.Client({ -- node: host, -- }); -+ node: host -+ }) - } -- return esClients.client; -+ return esClients.client - } - - /* -@@ -790,8 +819,8 @@ const getM2MToken = async () => { - return await m2m.getMachineToken( - config.AUTH0_CLIENT_ID, - config.AUTH0_CLIENT_SECRET -- ); --}; -+ ) -+} - - /* - * Function to get M2M token for U-Bahn -@@ -801,8 +830,8 @@ const getM2MUbahnToken = async () => { - return await m2mForUbahn.getMachineToken( - config.AUTH0_CLIENT_ID, - config.AUTH0_CLIENT_SECRET -- ); --}; -+ ) -+} - - /** - * Function to encode query string -@@ -810,17 +839,17 @@ const getM2MUbahnToken = async () => { - * @param {String} nesting the nesting string - * @returns {String} query string - */ --function encodeQueryString(queryObj, nesting = '') { -+function encodeQueryString (queryObj, nesting = '') { - const pairs = Object.entries(queryObj).map(([key, val]) => { - // Handle the nested, recursive case, where the value to encode is an object itself - if (typeof val === 'object') { -- return encodeQueryString(val, nesting + `${key}.`); -+ return encodeQueryString(val, nesting + `${key}.`) - } else { - // Handle base case, where the value to encode is simply a string. -- return [nesting + key, val].map(querystring.escape).join('='); -+ return [nesting + key, val].map(querystring.escape).join('=') - } -- }); -- return pairs.join('&'); -+ }) -+ return pairs.join('&') - } - - /** -@@ -828,31 +857,31 @@ function encodeQueryString(queryObj, nesting = '') { - * @param {Integer} externalId the legacy user id - * @returns {Array} the users found - */ --async function listUsersByExternalId(externalId) { -+async function listUsersByExternalId (externalId) { - // return empty list if externalId is null or undefined - if (!!externalId !== true) { -- return []; -+ return [] - } - -- const token = await getM2MUbahnToken(); -+ const token = await getM2MUbahnToken() - const q = { - enrich: true, - externalProfile: { - organizationId: config.ORG_ID, -- externalId, -- }, -- }; -- const url = `${config.TC_API}/users?${encodeQueryString(q)}`; -+ externalId -+ } -+ } -+ const url = `${config.TC_API}/users?${encodeQueryString(q)}` - const res = await request - .get(url) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'listUserByExternalId', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return res.body; -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return res.body - } - - /** -@@ -860,14 +889,14 @@ async function listUsersByExternalId(externalId) { - * @param {Integer} externalId the legacy user id - * @returns {Object} the user - */ --async function getUserByExternalId(externalId) { -- const users = await listUsersByExternalId(externalId); -+async function getUserByExternalId (externalId) { -+ const users = await listUsersByExternalId(externalId) - if (_.isEmpty(users)) { - throw new errors.NotFoundError( - `externalId: ${externalId} "user" not found` -- ); -+ ) - } -- return users[0]; -+ return users[0] - } - - /** -@@ -876,24 +905,24 @@ async function getUserByExternalId(externalId) { - * @params {Object} payload the payload - * @params {Object} options the extra options to control the function - */ --async function postEvent(topic, payload, options = {}) { -+async function postEvent (topic, payload, options = {}) { - logger.debug({ - component: 'helper', - context: 'postEvent', - message: `Posting event to Kafka topic ${topic}, ${JSON.stringify( - payload -- )}`, -- }); -- const client = getBusApiClient(); -+ )}` -+ }) -+ const client = getBusApiClient() - const message = { - topic, - originator: config.KAFKA_MESSAGE_ORIGINATOR, - timestamp: new Date().toISOString(), - 'mime-type': 'application/json', -- payload, -- }; -- await client.postEvent(message); -- await eventDispatcher.handleEvent(topic, { value: payload, options }); -+ payload -+ } -+ await client.postEvent(message) -+ await eventDispatcher.handleEvent(topic, { value: payload, options }) - } - - /** -@@ -902,11 +931,11 @@ async function postEvent(topic, payload, options = {}) { - * @param {Object} err the err - * @returns {Boolean} the result - */ --function isDocumentMissingException(err) { -+function isDocumentMissingException (err) { - if (err.statusCode === 404 && err instanceof ESResponseError) { -- return true; -+ return true - } -- return false; -+ return false - } - - /** -@@ -915,34 +944,34 @@ function isDocumentMissingException(err) { - * @param {Object} criteria the search criteria - * @returns the request result - */ --async function getProjects(currentUser, criteria = {}) { -- let token; -+async function getProjects (currentUser, criteria = {}) { -+ let token - if (currentUser.hasManagePermission || currentUser.isMachine) { -- const m2mToken = await getM2MToken(); -- token = `Bearer ${m2mToken}`; -+ const m2mToken = await getM2MToken() -+ token = `Bearer ${m2mToken}` - } else { -- token = currentUser.jwtToken; -+ token = currentUser.jwtToken - } -- const url = `${config.TC_API}/projects?type=talent-as-a-service`; -+ const url = `${config.TC_API}/projects?type=talent-as-a-service` - const res = await request - .get(url) - .query(criteria) - .set('Authorization', token) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getProjects', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) - const result = _.map(res.body, (item) => { -- return _.pick(item, ['id', 'name', 'invites', 'members']); -- }); -+ return _.pick(item, ['id', 'name', 'invites', 'members']) -+ }) - return { - total: Number(_.get(res.headers, 'x-total')), - page: Number(_.get(res.headers, 'x-page')), - perPage: Number(_.get(res.headers, 'x-per-page')), -- result, -- }; -+ result -+ } - } - - /** -@@ -951,24 +980,24 @@ async function getProjects(currentUser, criteria = {}) { - * @param {String} userId the legacy user id - * @returns {Object} the user - */ --async function getTopcoderUserById(userId) { -- const token = await getM2MToken(); -+async function getTopcoderUserById (userId) { -+ const token = await getM2MToken() - const res = await request - .get(config.TOPCODER_USERS_API) - .query({ filter: `id=${userId}` }) - .set('Authorization', `Bearer ${token}`) -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getTopcoderUserById', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- const user = _.get(res.body, 'result.content[0]'); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ const user = _.get(res.body, 'result.content[0]') - if (!user) { - throw new errors.NotFoundError( - `userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}` -- ); -+ ) - } -- return user; -+ return user - } - - /** -@@ -976,31 +1005,31 @@ async function getTopcoderUserById(userId) { - * @param {String} userId the user id - * @returns the request result - */ --async function getUserById(userId, enrich) { -- const token = await getM2MUbahnToken(); -+async function getUserById (userId, enrich) { -+ const token = await getM2MUbahnToken() - const res = await request - .get(`${config.TC_API}/users/${userId}` + (enrich ? '?enrich=true' : '')) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getUserById', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) - -- const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']); -+ const user = _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']) - - if (enrich) { - user.skills = (res.body.skills || []).map((skillObj) => - _.pick(skillObj.skill, ['id', 'name']) -- ); -- const attributes = _.get(res, 'body.attributes', []); -+ ) -+ const attributes = _.get(res, 'body.attributes', []) - user.attributes = _.map(attributes, (attr) => - _.pick(attr, ['id', 'value', 'attribute.id', 'attribute.name']) -- ); -+ ) - } - -- return user; -+ return user - } - - /** -@@ -1008,19 +1037,19 @@ async function getUserById(userId, enrich) { - * @param {Object} data the user data - * @returns the request result - */ --async function createUbahnUser({ handle, firstName, lastName }) { -- const token = await getM2MUbahnToken(); -+async function createUbahnUser ({ handle, firstName, lastName }) { -+ const token = await getM2MUbahnToken() - const res = await request - .post(`${config.TC_API}/users`) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') -- .send({ handle, firstName, lastName }); -+ .send({ handle, firstName, lastName }) - localLogger.debug({ - context: 'createUbahnUser', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return _.pick(res.body, ['id']); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return _.pick(res.body, ['id']) - } - - /** -@@ -1028,21 +1057,21 @@ async function createUbahnUser({ handle, firstName, lastName }) { - * @param {String} userId the user id(with uuid format) - * @param {Object} data the profile data - */ --async function createUserExternalProfile( -+async function createUserExternalProfile ( - userId, - { organizationId, externalId } - ) { -- const token = await getM2MUbahnToken(); -+ const token = await getM2MUbahnToken() - const res = await request - .post(`${config.TC_API}/users/${userId}/externalProfiles`) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') -- .send({ organizationId, externalId: String(externalId) }); -+ .send({ organizationId, externalId: String(externalId) }) - localLogger.debug({ - context: 'createUserExternalProfile', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) - } - - /** -@@ -1050,23 +1079,23 @@ async function createUserExternalProfile( - * @param {Array} handles the handle array - * @returns the request result - */ --async function getMembers(handles) { -- const token = await getM2MToken(); -+async function getMembers (handles) { -+ const token = await getM2MToken() - const handlesStr = _.map(handles, (handle) => { -- return '%22' + handle.toLowerCase() + '%22'; -- }).join(','); -- const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]`; -+ return '%22' + handle.toLowerCase() + '%22' -+ }).join(',') -+ const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]` - - const res = await request - .get(url) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getMembers', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return res.body; -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return res.body - } - - /** -@@ -1075,36 +1104,36 @@ async function getMembers(handles) { - * @param {Number} id project id - * @returns the request result - */ --async function getProjectById(currentUser, id) { -- let token; -+async function getProjectById (currentUser, id) { -+ let token - if (currentUser.hasManagePermission || currentUser.isMachine) { -- const m2mToken = await getM2MToken(); -- token = `Bearer ${m2mToken}`; -+ const m2mToken = await getM2MToken() -+ token = `Bearer ${m2mToken}` - } else { -- token = currentUser.jwtToken; -+ token = currentUser.jwtToken - } -- const url = `${config.TC_API}/projects/${id}`; -+ const url = `${config.TC_API}/projects/${id}` - try { - const res = await request - .get(url) - .set('Authorization', token) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getProjectById', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return _.pick(res.body, ['id', 'name', 'invites', 'members']); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return _.pick(res.body, ['id', 'name', 'invites', 'members']) - } catch (err) { - if (err.status === HttpStatus.FORBIDDEN) { - throw new errors.ForbiddenError( - `You are not allowed to access the project with id ${id}` -- ); -+ ) - } - if (err.status === HttpStatus.NOT_FOUND) { -- throw new errors.NotFoundError(`id: ${id} project not found`); -+ throw new errors.NotFoundError(`id: ${id} project not found`) - } -- throw err; -+ throw err - } - } - -@@ -1115,33 +1144,33 @@ async function getProjectById(currentUser, id) { - * @param {Object} criteria the search criteria - * @returns the request result - */ --async function getTopcoderSkills(criteria) { -- const token = await getM2MUbahnToken(); -+async function getTopcoderSkills (criteria) { -+ const token = await getM2MUbahnToken() - try { - const res = await request - .get(`${config.TC_API}/skills`) - .query({ - skillProviderId: config.TOPCODER_SKILL_PROVIDER_ID, -- ...criteria, -+ ...criteria - }) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getTopcoderSkills', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) - return { - total: Number(_.get(res.headers, 'x-total')), - page: Number(_.get(res.headers, 'x-page')), - perPage: Number(_.get(res.headers, 'x-per-page')), -- result: res.body, -- }; -+ result: res.body -+ } - } catch (err) { - if (err.status === HttpStatus.BAD_REQUEST) { -- throw new errors.BadRequestError(err.response.body.message); -+ throw new errors.BadRequestError(err.response.body.message) - } -- throw err; -+ throw err - } - } - -@@ -1150,18 +1179,18 @@ async function getTopcoderSkills(criteria) { - * @param {String} skillId the skill Id - * @returns the request result - */ --async function getSkillById(skillId) { -- const token = await getM2MUbahnToken(); -+async function getSkillById (skillId) { -+ const token = await getM2MUbahnToken() - const res = await request - .get(`${config.TC_API}/skills/${skillId}`) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getSkillById', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return _.pick(res.body, ['id', 'name']); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return _.pick(res.body, ['id', 'name']) - } - - /** -@@ -1174,22 +1203,22 @@ async function getSkillById(skillId) { - * @params {Object} currentUser the user who perform this operation - * @returns {String} the ubahn user id - */ --async function ensureUbahnUserId(currentUser) { -+async function ensureUbahnUserId (currentUser) { - try { -- return (await getUserByExternalId(currentUser.userId)).id; -+ return (await getUserByExternalId(currentUser.userId)).id - } catch (err) { - if (!(err instanceof errors.NotFoundError)) { -- throw err; -+ throw err - } -- const topcoderUser = await getTopcoderUserById(currentUser.userId); -+ const topcoderUser = await getTopcoderUserById(currentUser.userId) - const user = await createUbahnUser( - _.pick(topcoderUser, ['handle', 'firstName', 'lastName']) -- ); -+ ) - await createUserExternalProfile(user.id, { - organizationId: config.ORG_ID, -- externalId: currentUser.userId, -- }); -- return user.id; -+ externalId: currentUser.userId -+ }) -+ return user.id - } - } - -@@ -1199,8 +1228,8 @@ async function ensureUbahnUserId(currentUser) { - * @param {String} jobId the job id - * @returns {Object} the job data - */ --async function ensureJobById(jobId) { -- return models.Job.findById(jobId); -+async function ensureJobById (jobId) { -+ return models.Job.findById(jobId) - } - - /** -@@ -1209,8 +1238,8 @@ async function ensureJobById(jobId) { - * @param {String} resourceBookingId the resourceBooking id - * @returns {Object} the resourceBooking data - */ --async function ensureResourceBookingById(resourceBookingId) { -- return models.ResourceBooking.findById(resourceBookingId); -+async function ensureResourceBookingById (resourceBookingId) { -+ return models.ResourceBooking.findById(resourceBookingId) - } - - /** -@@ -1218,8 +1247,8 @@ async function ensureResourceBookingById(resourceBookingId) { - * @param {String} workPeriodId the workPeriod id - * @returns the workPeriod data - */ --async function ensureWorkPeriodById(workPeriodId) { -- return models.WorkPeriod.findById(workPeriodId); -+async function ensureWorkPeriodById (workPeriodId) { -+ return models.WorkPeriod.findById(workPeriodId) - } - - /** -@@ -1228,24 +1257,24 @@ async function ensureWorkPeriodById(workPeriodId) { - * @param {String} jobId the user id - * @returns {Object} the user data - */ --async function ensureUserById(userId) { -- const token = await getM2MUbahnToken(); -+async function ensureUserById (userId) { -+ const token = await getM2MUbahnToken() - try { - const res = await request - .get(`${config.TC_API}/users/${userId}`) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'ensureUserById', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return res.body; -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return res.body - } catch (err) { - if (err.status === HttpStatus.NOT_FOUND) { -- throw new errors.NotFoundError(`id: ${userId} "user" not found`); -+ throw new errors.NotFoundError(`id: ${userId} "user" not found`) - } -- throw err; -+ throw err - } - } - -@@ -1254,12 +1283,12 @@ async function ensureUserById(userId) { - * - * @returns {Object} the M2M auth user - */ --function getAuditM2Muser() { -+function getAuditM2Muser () { - return { - isMachine: true, - userId: config.m2m.M2M_AUDIT_USER_ID, -- handle: config.m2m.M2M_AUDIT_HANDLE, -- }; -+ handle: config.m2m.M2M_AUDIT_HANDLE -+ } - } - - /** -@@ -1271,24 +1300,24 @@ function getAuditM2Muser() { - * @param {Number} projectId project id - * @returns the result - */ --async function checkIsMemberOfProject(userId, projectId) { -- const m2mToken = await getM2MToken(); -+async function checkIsMemberOfProject (userId, projectId) { -+ const m2mToken = await getM2MToken() - const res = await request - .get(`${config.TC_API}/projects/${projectId}`) - .set('Authorization', `Bearer ${m2mToken}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -- const memberIdList = _.map(res.body.members, 'userId'); -+ .set('Accept', 'application/json') -+ const memberIdList = _.map(res.body.members, 'userId') - localLogger.debug({ - context: 'checkIsMemberOfProject', - message: `the members of project ${projectId}: ${JSON.stringify( - memberIdList -- )}, authUserId: ${JSON.stringify(userId)}`, -- }); -+ )}, authUserId: ${JSON.stringify(userId)}` -+ }) - if (!memberIdList.includes(userId)) { - throw new errors.UnauthorizedError( - `userId: ${userId} the user is not a member of project ${projectId}` -- ); -+ ) - } - } - -@@ -1298,11 +1327,11 @@ async function checkIsMemberOfProject(userId, projectId) { - * @param {Array} handles the array of handles - * @returns {Array} the member details - */ --async function getMemberDetailsByHandles(handles) { -+async function getMemberDetailsByHandles (handles) { - if (!handles.length) { -- return []; -+ return [] - } -- const token = await getM2MToken(); -+ const token = await getM2MToken() - const res = await request - .get(`${config.TOPCODER_MEMBERS_API}/_search`) - .query({ -@@ -1310,15 +1339,15 @@ async function getMemberDetailsByHandles(handles) { - handles, - (handle) => `handleLower:${handle.toLowerCase()}` - ).join(' OR '), -- fields: 'userId,handle,firstName,lastName,email', -+ fields: 'userId,handle,firstName,lastName,email' - }) - .set('Authorization', `Bearer ${token}`) -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getMemberDetailsByHandles', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return _.get(res.body, 'result.content'); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return _.get(res.body, 'result.content') - } - - /** -@@ -1327,17 +1356,17 @@ async function getMemberDetailsByHandles(handles) { - * @param {String} handle the user handle - * @returns {Object} the member details - */ --async function getV3MemberDetailsByHandle(handle) { -- const token = await getM2MToken(); -+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'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getV3MemberDetailsByHandle', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return _.get(res.body, 'result.content'); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return _.get(res.body, 'result.content') - } - - /** -@@ -1347,20 +1376,20 @@ async function getV3MemberDetailsByHandle(handle) { - * @param {String} email the email - * @returns {Array} the member details - */ --async function _getMemberDetailsByEmail(token, email) { -+async function _getMemberDetailsByEmail (token, email) { - const res = await request - .get(config.TOPCODER_USERS_API) - .query({ - filter: `email=${email}`, -- fields: 'handle,id,email,firstName,lastName', -+ fields: 'handle,id,email,firstName,lastName' - }) - .set('Authorization', `Bearer ${token}`) -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: '_getMemberDetailsByEmail', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return _.get(res.body, 'result.content'); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return _.get(res.body, 'result.content') - } - - /** -@@ -1370,25 +1399,25 @@ async function _getMemberDetailsByEmail(token, email) { - * @param {Array} emails the array of emails - * @returns {Array} the member details - */ --async function getMemberDetailsByEmails(emails) { -- const token = await getM2MToken(); -+async function getMemberDetailsByEmails (emails) { -+ const token = await getM2MToken() - const limiter = new Bottleneck({ -- maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API, -- }); -+ maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API -+ }) - const membersArray = await Promise.all( - emails.map((email) => - limiter.schedule(() => - _getMemberDetailsByEmail(token, email).catch((error) => { - localLogger.error({ - context: 'getMemberDetailsByEmails', -- message: error.message, -- }); -- return []; -+ message: error.message -+ }) -+ return [] - }) - ) - ) -- ); -- return _.flatten(membersArray); -+ ) -+ return _.flatten(membersArray) - } - - /** -@@ -1399,20 +1428,20 @@ async function getMemberDetailsByEmails(emails) { - * @param {Object} criteria the filtering criteria - * @returns {Object} the member created - */ --async function createProjectMember(projectId, data, criteria) { -- const m2mToken = await getM2MToken(); -+async function createProjectMember (projectId, data, criteria) { -+ const m2mToken = await getM2MToken() - const { body: member } = await request - .post(`${config.TC_API}/projects/${projectId}/members`) - .set('Authorization', `Bearer ${m2mToken}`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - .query(criteria) -- .send(data); -+ .send(data) - localLogger.debug({ - context: 'createProjectMember', -- message: `response body: ${JSON.stringify(member)}`, -- }); -- return member; -+ message: `response body: ${JSON.stringify(member)}` -+ }) -+ return member - } - - /** -@@ -1422,21 +1451,21 @@ async function createProjectMember(projectId, data, criteria) { - * @param {Object} criteria the search criteria - * @returns {Array} the project members - */ --async function listProjectMembers(currentUser, projectId, criteria = {}) { -+async function listProjectMembers (currentUser, projectId, criteria = {}) { - const token = - currentUser.hasManagePermission || currentUser.isMachine - ? `Bearer ${await getM2MToken()}` -- : currentUser.jwtToken; -+ : currentUser.jwtToken - const { body: members } = await request - .get(`${config.TC_API}/projects/${projectId}/members`) - .query(criteria) - .set('Authorization', token) -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'listProjectMembers', -- message: `response body: ${JSON.stringify(members)}`, -- }); -- return members; -+ message: `response body: ${JSON.stringify(members)}` -+ }) -+ return members - } - - /** -@@ -1446,21 +1475,21 @@ async function listProjectMembers(currentUser, projectId, criteria = {}) { - * @param {Object} criteria the search criteria - * @returns {Array} the member invites - */ --async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { -+async function listProjectMemberInvites (currentUser, projectId, criteria = {}) { - const token = - currentUser.hasManagePermission || currentUser.isMachine - ? `Bearer ${await getM2MToken()}` -- : currentUser.jwtToken; -+ : currentUser.jwtToken - const { body: invites } = await request - .get(`${config.TC_API}/projects/${projectId}/invites`) - .query(criteria) - .set('Authorization', token) -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'listProjectMemberInvites', -- message: `response body: ${JSON.stringify(invites)}`, -- }); -- return invites; -+ message: `response body: ${JSON.stringify(invites)}` -+ }) -+ return invites - } - - /** -@@ -1470,24 +1499,24 @@ async function listProjectMemberInvites(currentUser, projectId, criteria = {}) { - * @param {String} projectMemberId the id of the project member - * @returns {undefined} - */ --async function deleteProjectMember(currentUser, projectId, projectMemberId) { -+async function deleteProjectMember (currentUser, projectId, projectMemberId) { - const token = - currentUser.hasManagePermission || currentUser.isMachine - ? `Bearer ${await getM2MToken()}` -- : currentUser.jwtToken; -+ : currentUser.jwtToken - try { - await request - .delete( - `${config.TC_API}/projects/${projectId}/members/${projectMemberId}` - ) -- .set('Authorization', token); -+ .set('Authorization', token) - } catch (err) { - if (err.status === HttpStatus.NOT_FOUND) { - throw new errors.NotFoundError( - `projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}` -- ); -+ ) - } -- throw err; -+ throw err - } - } - -@@ -1497,13 +1526,13 @@ async function deleteProjectMember(currentUser, projectId, projectMemberId) { - * @param {String} attributeName Requested attribute name, e.g. "email" - * @returns attribute value - */ --function getUserAttributeValue(user, attributeName) { -- const attributes = _.get(user, 'attributes', []); -+function getUserAttributeValue (user, attributeName) { -+ const attributes = _.get(user, 'attributes', []) - const targetAttribute = _.find( - attributes, - (a) => a.attribute.name === attributeName -- ); -- return _.get(targetAttribute, 'value'); -+ ) -+ return _.get(targetAttribute, 'value') - } - - /** -@@ -1513,34 +1542,34 @@ function getUserAttributeValue(user, attributeName) { - * @param {String} token m2m token - * @returns {Object} the challenge created - */ --async function createChallenge(data, token) { -+async function createChallenge (data, token) { - if (!token) { -- token = await getM2MToken(); -+ token = await getM2MToken() - } -- const url = `${config.TC_API}/challenges`; -+ const url = `${config.TC_API}/challenges` - localLogger.debug({ - context: 'createChallenge', -- message: `EndPoint: POST ${url}`, -- }); -+ message: `EndPoint: POST ${url}` -+ }) - localLogger.debug({ - context: 'createChallenge', -- message: `Request Body: ${JSON.stringify(data)}`, -- }); -+ message: `Request Body: ${JSON.stringify(data)}` -+ }) - const { body: challenge, status: httpStatus } = await request - .post(url) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') -- .send(data); -+ .send(data) - localLogger.debug({ - context: 'createChallenge', -- message: `Status Code: ${httpStatus}`, -- }); -+ message: `Status Code: ${httpStatus}` -+ }) - localLogger.debug({ - context: 'createChallenge', -- message: `Response Body: ${JSON.stringify(challenge)}`, -- }); -- return challenge; -+ message: `Response Body: ${JSON.stringify(challenge)}` -+ }) -+ return challenge - } - - /** -@@ -1551,34 +1580,34 @@ async function createChallenge(data, token) { - * @param {String} token m2m token - * @returns {Object} the challenge updated - */ --async function updateChallenge(challengeId, data, token) { -+async function updateChallenge (challengeId, data, token) { - if (!token) { -- token = await getM2MToken(); -+ token = await getM2MToken() - } -- const url = `${config.TC_API}/challenges/${challengeId}`; -+ const url = `${config.TC_API}/challenges/${challengeId}` - localLogger.debug({ - context: 'updateChallenge', -- message: `EndPoint: PATCH ${url}`, -- }); -+ message: `EndPoint: PATCH ${url}` -+ }) - localLogger.debug({ - context: 'updateChallenge', -- message: `Request Body: ${JSON.stringify(data)}`, -- }); -+ message: `Request Body: ${JSON.stringify(data)}` -+ }) - const { body: challenge, status: httpStatus } = await request - .patch(url) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') -- .send(data); -+ .send(data) - localLogger.debug({ - context: 'updateChallenge', -- message: `Status Code: ${httpStatus}`, -- }); -+ message: `Status Code: ${httpStatus}` -+ }) - localLogger.debug({ - context: 'updateChallenge', -- message: `Response Body: ${JSON.stringify(challenge)}`, -- }); -- return challenge; -+ message: `Response Body: ${JSON.stringify(challenge)}` -+ }) -+ return challenge - } - - /** -@@ -1588,34 +1617,34 @@ async function updateChallenge(challengeId, data, token) { - * @param {String} token m2m token - * @returns {Object} the resource created - */ --async function createChallengeResource(data, token) { -+async function createChallengeResource (data, token) { - if (!token) { -- token = await getM2MToken(); -+ token = await getM2MToken() - } -- const url = `${config.TC_API}/resources`; -+ const url = `${config.TC_API}/resources` - localLogger.debug({ - context: 'createChallengeResource', -- message: `EndPoint: POST ${url}`, -- }); -+ message: `EndPoint: POST ${url}` -+ }) - localLogger.debug({ - context: 'createChallengeResource', -- message: `Request Body: ${JSON.stringify(data)}`, -- }); -+ message: `Request Body: ${JSON.stringify(data)}` -+ }) - const { body: resource, status: httpStatus } = await request - .post(url) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') -- .send(data); -+ .send(data) - localLogger.debug({ - context: 'createChallengeResource', -- message: `Status Code: ${httpStatus}`, -- }); -+ message: `Status Code: ${httpStatus}` -+ }) - localLogger.debug({ - context: 'createChallengeResource', -- message: `Response Body: ${JSON.stringify(resource)}`, -- }); -- return resource; -+ message: `Response Body: ${JSON.stringify(resource)}` -+ }) -+ return resource - } - - /** -@@ -1624,40 +1653,40 @@ async function createChallengeResource(data, token) { - * @param {Date} end end date of the resource booking - * @returns {Array<{startDate:Date, endDate:Date, daysWorked:number}>} information about workPeriods - */ --function extractWorkPeriods(start, end) { -+function extractWorkPeriods (start, end) { - // calculate maximum possible daysWorked for a week -- function getDaysWorked(week) { -+ function getDaysWorked (week) { - if (weeks === 1) { -- return Math.min(endDay, 5) - Math.max(startDay, 1) + 1; -+ return Math.min(endDay, 5) - Math.max(startDay, 1) + 1 - } else if (week === 0) { -- return Math.min(6 - startDay, 5); -+ return Math.min(6 - startDay, 5) - } else if (week === weeks - 1) { -- return Math.min(endDay, 5); -- } else return 5; -+ return Math.min(endDay, 5) -+ } else return 5 - } -- const periods = []; -+ const periods = [] - if (_.isNil(start) || _.isNil(end)) { -- return periods; -+ return periods - } -- const startDate = moment(start); -- const startDay = startDate.get('day'); -- startDate.set('day', 0).startOf('day'); -+ const startDate = moment(start) -+ const startDay = startDate.get('day') -+ startDate.set('day', 0).startOf('day') - -- const endDate = moment(end); -- const endDay = endDate.get('day'); -- endDate.set('day', 6).endOf('day'); -+ const endDate = moment(end) -+ const endDay = endDate.get('day') -+ endDate.set('day', 6).endOf('day') - -- const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7; -+ const weeks = Math.round(moment.duration(endDate - startDate).asDays()) / 7 - - for (let i = 0; i < weeks; i++) { - periods.push({ - startDate: startDate.format('YYYY-MM-DD'), - endDate: startDate.add(6, 'day').format('YYYY-MM-DD'), -- daysWorked: getDaysWorked(i), -- }); -- startDate.add(1, 'day'); -+ daysWorked: getDaysWorked(i) -+ }) -+ startDate.add(1, 'day') - } -- return periods; -+ return periods - } - - /** -@@ -1666,19 +1695,19 @@ function extractWorkPeriods(start, end) { - * @param {String} userHandle user handle - * @returns {String} email address of the user - */ --async function getUserByHandle(userHandle) { -- const token = await getM2MToken(); -- const url = `${config.TC_API}/members/${userHandle}`; -+async function getUserByHandle (userHandle) { -+ const token = await getM2MToken() -+ const url = `${config.TC_API}/members/${userHandle}` - const res = await request - .get(url) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') -- .set('Accept', 'application/json'); -+ .set('Accept', 'application/json') - localLogger.debug({ - context: 'getUserByHandle', -- message: `response body: ${JSON.stringify(res.body)}`, -- }); -- return _.get(res, 'body'); -+ message: `response body: ${JSON.stringify(res.body)}` -+ }) -+ return _.get(res, 'body') - } - - /** -@@ -1687,14 +1716,14 @@ async function getUserByHandle(userHandle) { - * @param {*} object of json that would be replaced in string - * @returns - */ --async function substituteStringByObject(string, object) { -+async function substituteStringByObject (string, object) { - for (var key in object) { - if (!Object.prototype.hasOwnProperty.call(object, key)) { -- continue; -+ continue - } -- string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]); -+ string = string.replace(new RegExp('{{' + key + '}}', 'g'), object[key]) - } -- return string; -+ return string - } - - /** -@@ -1702,19 +1731,19 @@ async function substituteStringByObject(string, object) { - * @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 (currentUser, data) { -+ const token = currentUser.jwtToken - const res = await request - .post(`${config.TC_API}/projects/`) - .set('Authorization', token) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') -- .send(data); -+ .send(data) - localLogger.debug({ - context: 'createProject', -- message: `response body: ${JSON.stringify(res)}`, -- }); -- return _.get(res, 'body'); -+ message: `response body: ${JSON.stringify(res)}` -+ }) -+ return _.get(res, 'body') - } - - module.exports = { -@@ -1733,9 +1762,9 @@ module.exports = { - getUserId: async (userId) => { - // check m2m user id - if (userId === config.m2m.M2M_AUDIT_USER_ID) { -- return config.m2m.M2M_AUDIT_USER_ID; -+ return config.m2m.M2M_AUDIT_USER_ID - } -- return ensureUbahnUserId({ userId }); -+ return ensureUbahnUserId({ userId }) - }, - getUserByExternalId, - getM2MToken, -@@ -1769,5 +1798,5 @@ module.exports = { - extractWorkPeriods, - getUserByHandle, - substituteStringByObject, -- createProject, --}; -+ createProject -+} -diff --git a/src/controllers/RoleController.js b/src/controllers/RoleController.js -new file mode 100644 -index 0000000..747cbe4 ---- /dev/null -+++ b/src/controllers/RoleController.js -@@ -0,0 +1,59 @@ -+/** -+ * Controller for Role endpoints -+ */ -+const HttpStatus = require('http-status-codes') -+const service = require('../services/RoleService') -+ -+/** -+ * Get role by id -+ * @param req the request -+ * @param res the response -+ */ -+async function getRole (req, res) { -+ res.send(await service.getRole(req.authUser, req.params.id, req.query.fromDb)) -+} -+ -+/** -+ * Create role -+ * @param req the request -+ * @param res the response -+ */ -+async function createRole (req, res) { -+ res.send(await service.createRole(req.authUser, req.body)) -+} -+ -+/** -+ * update role by id -+ * @param req the request -+ * @param res the response -+ */ -+async function updateRole (req, res) { -+ res.send(await service.updateRole(req.authUser, req.params.id, req.body)) -+} -+ -+/** -+ * Delete role by id -+ * @param req the request -+ * @param res the response -+ */ -+async function deleteRole (req, res) { -+ await service.deleteRole(req.authUser, req.params.id) -+ res.status(HttpStatus.NO_CONTENT).end() -+} -+ -+/** -+ * Search roles -+ * @param req the request -+ * @param res the response -+ */ -+async function searchRoles (req, res) { -+ res.send(await service.searchRoles(req.authUser, req.query)) -+} -+ -+module.exports = { -+ getRole, -+ createRole, -+ updateRole, -+ deleteRole, -+ searchRoles -+} -diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js -index ca4f1bc..26d7073 100644 ---- a/src/controllers/TeamController.js -+++ b/src/controllers/TeamController.js -@@ -1,19 +1,19 @@ - /** - * Controller for TaaS teams endpoints - */ --const HttpStatus = require('http-status-codes'); --const service = require('../services/TeamService'); --const helper = require('../common/helper'); -+const HttpStatus = require('http-status-codes') -+const service = require('../services/TeamService') -+const helper = require('../common/helper') - - /** - * Search teams - * @param req the request - * @param res the response - */ --async function searchTeams(req, res) { -- const result = await service.searchTeams(req.authUser, req.query); -- helper.setResHeaders(req, res, result); -- res.send(result.result); -+async function searchTeams (req, res) { -+ const result = await service.searchTeams(req.authUser, req.query) -+ helper.setResHeaders(req, res, result) -+ res.send(result.result) - } - - /** -@@ -21,8 +21,8 @@ async function searchTeams(req, res) { - * @param req the request - * @param res the response - */ --async function getTeam(req, res) { -- res.send(await service.getTeam(req.authUser, req.params.id)); -+async function getTeam (req, res) { -+ res.send(await service.getTeam(req.authUser, req.params.id)) - } - - /** -@@ -30,10 +30,10 @@ async function getTeam(req, res) { - * @param req the request - * @param res the response - */ --async function getTeamJob(req, res) { -+async function getTeamJob (req, res) { - res.send( - await service.getTeamJob(req.authUser, req.params.id, req.params.jobId) -- ); -+ ) - } - - /** -@@ -41,9 +41,9 @@ async function getTeamJob(req, res) { - * @param req the request - * @param res the response - */ --async function sendEmail(req, res) { -- await service.sendEmail(req.authUser, req.body); -- res.status(HttpStatus.NO_CONTENT).end(); -+async function sendEmail (req, res) { -+ await service.sendEmail(req.authUser, req.body) -+ res.status(HttpStatus.NO_CONTENT).end() - } - - /** -@@ -51,10 +51,10 @@ async function sendEmail(req, res) { - * @param req the request - * @param res the response - */ --async function addMembers(req, res) { -+async function addMembers (req, res) { - res.send( - await service.addMembers(req.authUser, req.params.id, req.query, req.body) -- ); -+ ) - } - - /** -@@ -62,13 +62,13 @@ async function addMembers(req, res) { - * @param req the request - * @param res the response - */ --async function searchMembers(req, res) { -+async function searchMembers (req, res) { - const result = await service.searchMembers( - req.authUser, - req.params.id, - req.query -- ); -- res.send(result.result); -+ ) -+ res.send(result.result) - } - - /** -@@ -76,13 +76,13 @@ async function searchMembers(req, res) { - * @param req the request - * @param res the response - */ --async function searchInvites(req, res) { -+async function searchInvites (req, res) { - const result = await service.searchInvites( - req.authUser, - req.params.id, - req.query -- ); -- res.send(result.result); -+ ) -+ res.send(result.result) - } - - /** -@@ -90,13 +90,13 @@ async function searchInvites(req, res) { - * @param req the request - * @param res the response - */ --async function deleteMember(req, res) { -+async function deleteMember (req, res) { - await service.deleteMember( - req.authUser, - req.params.id, - req.params.projectMemberId -- ); -- res.status(HttpStatus.NO_CONTENT).end(); -+ ) -+ res.status(HttpStatus.NO_CONTENT).end() - } - - /** -@@ -104,8 +104,8 @@ async function deleteMember(req, res) { - * @param req the request - * @param res the response - */ --async function getMe(req, res) { -- res.send(await service.getMe(req.authUser)); -+async function getMe (req, res) { -+ res.send(await service.getMe(req.authUser)) - } - - /** -@@ -113,8 +113,8 @@ async function getMe(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 createProj (req, res) { -+ res.send(await service.createProj(req.authUser, req.body)) - } - - module.exports = { -@@ -127,5 +127,5 @@ module.exports = { - searchInvites, - deleteMember, - getMe, -- createProj, --}; -+ createProj -+} -diff --git a/src/eventHandlers/RoleEventHandler.js b/src/eventHandlers/RoleEventHandler.js -new file mode 100644 -index 0000000..38dbdb7 ---- /dev/null -+++ b/src/eventHandlers/RoleEventHandler.js -@@ -0,0 +1,64 @@ -+/* -+ * Handle events for ResourceBooking. -+ */ -+ -+const { Op } = require('sequelize') -+const _ = require('lodash') -+const models = require('../models') -+const logger = require('../common/logger') -+const helper = require('../common/helper') -+const JobService = require('../services/JobService') -+ -+const Job = models.Job -+ -+/** -+ * When a Role is deleted, jobs related to -+ * that role should be updated -+ * @param {object} payload the event payload -+ * @returns {undefined} -+ */ -+async function updateJobs (payload) { -+ // find jobs have this role -+ const jobs = await Job.findAll({ -+ where: { -+ roleIds: { [Op.contains]: [payload.value.id] } -+ }, -+ raw: true -+ }) -+ if (jobs.length === 0) { -+ logger.debug({ -+ component: 'RoleEventHandler', -+ context: 'updateJobs', -+ message: `id: ${payload.value.id} role has no related job - ignored` -+ }) -+ return -+ } -+ const m2mUser = helper.getAuditM2Muser() -+ // remove role id from related jobs -+ await Promise.all(_.map(jobs, async job => { -+ let roleIds = _.filter(job.roleIds, roleId => roleId !== payload.value.id) -+ if (roleIds.length === 0) { -+ roleIds = null -+ } -+ await JobService.partiallyUpdateJob(m2mUser, job.id, { roleIds }) -+ })) -+ logger.debug({ -+ component: 'RoleEventHandler', -+ context: 'updateJobs', -+ message: `role id: ${payload.value.id} removed from jobs with id: ${_.map(jobs, 'id')}` -+ }) -+} -+ -+/** -+ * Process role delete event. -+ * -+ * @param {Object} payload the event payload -+ * @returns {undefined} -+ */ -+async function processDelete (payload) { -+ await updateJobs(payload) -+} -+ -+module.exports = { -+ processDelete -+} -diff --git a/src/eventHandlers/index.js b/src/eventHandlers/index.js -index 1744599..6e0ec2a 100644 ---- a/src/eventHandlers/index.js -+++ b/src/eventHandlers/index.js -@@ -8,6 +8,7 @@ const JobEventHandler = require('./JobEventHandler') - const JobCandidateEventHandler = require('./JobCandidateEventHandler') - const ResourceBookingEventHandler = require('./ResourceBookingEventHandler') - const InterviewEventHandler = require('./InterviewEventHandler') -+const RoleEventHandler = require('./RoleEventHandler') - const logger = require('../common/logger') - - const TopicOperationMapping = { -@@ -16,7 +17,8 @@ const TopicOperationMapping = { - [config.TAAS_RESOURCE_BOOKING_CREATE_TOPIC]: ResourceBookingEventHandler.processCreate, - [config.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC]: ResourceBookingEventHandler.processUpdate, - [config.TAAS_RESOURCE_BOOKING_DELETE_TOPIC]: ResourceBookingEventHandler.processDelete, -- [config.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewEventHandler.processRequest -+ [config.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewEventHandler.processRequest, -+ [config.TAAS_ROLE_DELETE_TOPIC]: RoleEventHandler.processDelete - } - - /** -diff --git a/src/models/Job.js b/src/models/Job.js -index 49d34ff..66f15b0 100644 ---- a/src/models/Job.js -+++ b/src/models/Job.js -@@ -104,6 +104,12 @@ module.exports = (sequelize) => { - defaultValue: false, - allowNull: false - }, -+ roleIds: { -+ field: 'role_ids', -+ type: Sequelize.ARRAY({ -+ type: Sequelize.UUID -+ }) -+ }, - createdBy: { - field: 'created_by', - type: Sequelize.UUID, -diff --git a/src/models/Role.js b/src/models/Role.js -new file mode 100644 -index 0000000..57cd502 ---- /dev/null -+++ b/src/models/Role.js -@@ -0,0 +1,165 @@ -+const { Sequelize, Model } = require('sequelize') -+const config = require('config') -+const errors = require('../common/errors') -+ -+module.exports = (sequelize) => { -+ class Role extends Model { -+ /** -+ * Get role by id -+ * @param {String} id the role id -+ * @returns {Role} the role instance -+ */ -+ static async findById (id) { -+ const role = await Role.findOne({ -+ where: { -+ id -+ } -+ }) -+ if (!role) { -+ throw new errors.NotFoundError(`id: ${id} "Role" doesn't exists.`) -+ } -+ return role -+ } -+ } -+ Role.init( -+ { -+ id: { -+ type: Sequelize.UUID, -+ primaryKey: true, -+ allowNull: false, -+ defaultValue: Sequelize.UUIDV4 -+ }, -+ name: { -+ type: Sequelize.STRING(50), -+ allowNull: false -+ }, -+ description: { -+ type: Sequelize.STRING(1000) -+ }, -+ listOfSkills: { -+ field: 'list_of_skills', -+ type: Sequelize.ARRAY({ -+ type: Sequelize.STRING(50) -+ }) -+ }, -+ rates: { -+ type: Sequelize.ARRAY({ -+ type: Sequelize.JSONB({ -+ global: { -+ type: Sequelize.SMALLINT, -+ allowNull: false -+ }, -+ inCountry: { -+ field: 'in_country', -+ type: Sequelize.SMALLINT, -+ allowNull: false -+ }, -+ offShore: { -+ field: 'off_shore', -+ type: Sequelize.SMALLINT, -+ allowNull: false -+ }, -+ rate30Global: { -+ field: 'rate30_global', -+ type: Sequelize.SMALLINT -+ }, -+ rate30InCountry: { -+ field: 'rate30_in_country', -+ type: Sequelize.SMALLINT -+ }, -+ rate30OffShore: { -+ field: 'rate30_off_shore', -+ type: Sequelize.SMALLINT -+ }, -+ rate20Global: { -+ field: 'rate20_global', -+ type: Sequelize.SMALLINT -+ }, -+ rate20InCountry: { -+ field: 'rate20_in_country', -+ type: Sequelize.SMALLINT -+ }, -+ rate20OffShore: { -+ field: 'rate20_off_shore', -+ type: Sequelize.SMALLINT -+ } -+ }), -+ allowNull: false -+ }), -+ allowNull: false -+ }, -+ numberOfMembers: { -+ field: 'number_of_members', -+ type: Sequelize.NUMERIC -+ }, -+ numberOfMembersAvailable: { -+ field: 'number_of_members_available', -+ type: Sequelize.SMALLINT -+ }, -+ imageUrl: { -+ field: 'image_url', -+ type: Sequelize.STRING(255) -+ }, -+ timeToCandidate: { -+ field: 'time_to_candidate', -+ type: Sequelize.SMALLINT -+ }, -+ timeToInterview: { -+ field: 'time_to_interview', -+ type: Sequelize.SMALLINT -+ }, -+ 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: 'roles', -+ paranoid: true, -+ deletedAt: 'deletedAt', -+ createdAt: 'createdAt', -+ updatedAt: 'updatedAt', -+ timestamps: true, -+ defaultScope: { -+ attributes: { -+ exclude: ['deletedAt'] -+ } -+ }, -+ hooks: { -+ afterCreate: (role) => { -+ delete role.dataValues.deletedAt -+ } -+ }, -+ indexes: [ -+ { -+ unique: true, -+ fields: ['name'], -+ where: { -+ deleted_at: null -+ } -+ } -+ ] -+ } -+ ) -+ -+ return Role -+} -diff --git a/src/routes/RoleRoutes.js b/src/routes/RoleRoutes.js -new file mode 100644 -index 0000000..2fb6d55 ---- /dev/null -+++ b/src/routes/RoleRoutes.js -@@ -0,0 +1,41 @@ -+/** -+ * Contains role routes -+ */ -+const constants = require('../../app-constants') -+ -+module.exports = { -+ '/roles': { -+ post: { -+ controller: 'RoleController', -+ method: 'createRole', -+ auth: 'jwt', -+ scopes: [constants.Scopes.CREATE_ROLE, constants.Scopes.ALL_ROLE] -+ }, -+ get: { -+ controller: 'RoleController', -+ method: 'searchRoles', -+ auth: 'jwt', -+ scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] -+ } -+ }, -+ '/roles/:id': { -+ get: { -+ controller: 'RoleController', -+ method: 'getRole', -+ auth: 'jwt', -+ scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] -+ }, -+ patch: { -+ controller: 'RoleController', -+ method: 'updateRole', -+ auth: 'jwt', -+ scopes: [constants.Scopes.UPDATE_ROLE, constants.Scopes.ALL_ROLE] -+ }, -+ delete: { -+ controller: 'RoleController', -+ method: 'deleteRole', -+ auth: 'jwt', -+ scopes: [constants.Scopes.DELETE_ROLE, constants.Scopes.ALL_ROLE] -+ } -+ } -+} -diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js -index 9bbe25c..07d777d 100644 ---- a/src/routes/TeamRoutes.js -+++ b/src/routes/TeamRoutes.js -@@ -1,7 +1,7 @@ - /** - * Contains taas team routes - */ --const constants = require('../../app-constants'); -+const constants = require('../../app-constants') - - module.exports = { - '/taas-teams': { -@@ -9,85 +9,85 @@ module.exports = { - controller: 'TeamController', - method: 'searchTeams', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/email': { - post: { - controller: 'TeamController', - method: 'sendEmail', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/skills': { - get: { - controller: 'SkillController', - method: 'searchSkills', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/me': { - get: { - controller: 'TeamController', - method: 'getMe', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/:id': { - get: { - controller: 'TeamController', - method: 'getTeam', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/:id/jobs/:jobId': { - get: { - controller: 'TeamController', - method: 'getTeamJob', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/:id/members': { - post: { - controller: 'TeamController', - method: 'addMembers', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -+ scopes: [constants.Scopes.READ_TAAS_TEAM] - }, - get: { - controller: 'TeamController', - method: 'searchMembers', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/:id/invites': { - get: { - controller: 'TeamController', - method: 'searchInvites', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/:id/members/:projectMemberId': { - delete: { - controller: 'TeamController', - method: 'deleteMember', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } - }, - '/taas-teams/createTeamRequest': { - post: { - controller: 'TeamController', - method: 'createProj', - auth: 'jwt', -- scopes: [constants.Scopes.READ_TAAS_TEAM], -- }, -- }, --}; -+ scopes: [constants.Scopes.READ_TAAS_TEAM] -+ } -+ } -+} -diff --git a/src/services/InterviewService.js b/src/services/InterviewService.js -index 10a065f..a69a788 100644 ---- a/src/services/InterviewService.js -+++ b/src/services/InterviewService.js -@@ -241,8 +241,8 @@ async function requestInterview (currentUser, jobCandidateId, interview) { - 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] -+ var foundGuestMember = _.find(guestMembers, function (guestMember) { return guestEmail === guestMember.email }) -+ return (foundGuestMember !== undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split('@')[0] - }) - - try { -diff --git a/src/services/JobService.js b/src/services/JobService.js -index 7d855bd..be5dfde 100644 ---- a/src/services/JobService.js -+++ b/src/services/JobService.js -@@ -74,6 +74,27 @@ async function _validateSkills (skills) { - } - } - -+/** -+ * Validate if all roles exist. -+ * -+ * @param {Array} roles the list of roles -+ * @returns {undefined} -+ */ -+async function _validateRoles (roles) { -+ const foundRolesObj = await models.Role.findAll({ -+ where: { -+ id: roles -+ }, -+ attributes: ['id'], -+ raw: true -+ }) -+ const foundRoles = _.map(foundRolesObj, 'id') -+ const nonexistentRoles = _.difference(roles, foundRoles) -+ if (nonexistentRoles.length > 0) { -+ throw new errors.BadRequestError(`Invalid roles: [${nonexistentRoles}]`) -+ } -+} -+ - /** - * Check user permission for getting job. - * -@@ -154,6 +175,10 @@ async function createJob (currentUser, job) { - } - - await _validateSkills(job.skills) -+ if (job.roleIds) { -+ job.roleIds = _.uniq(job.roleIds) -+ await _validateRoles(job.roleIds) -+ } - job.id = uuid() - job.createdBy = await helper.getUserId(currentUser.userId) - -@@ -177,7 +202,8 @@ createJob.schema = Joi.object().keys({ - rateType: Joi.rateType().allow(null), - workload: Joi.workload().allow(null), - skills: Joi.array().items(Joi.string().uuid()).required(), -- isApplicationPageActive: Joi.boolean() -+ isApplicationPageActive: Joi.boolean(), -+ roleIds: Joi.array().items(Joi.string().uuid().required()) - }).required() - }).required() - -@@ -192,6 +218,10 @@ async function updateJob (currentUser, id, data) { - if (data.skills) { - await _validateSkills(data.skills) - } -+ if (data.roleIds) { -+ data.roleIds = _.uniq(data.roleIds) -+ await _validateRoles(data.roleIds) -+ } - let job = await Job.findById(id) - const oldValue = job.toJSON() - -@@ -245,7 +275,8 @@ partiallyUpdateJob.schema = Joi.object().keys({ - rateType: Joi.rateType().allow(null), - workload: Joi.workload().allow(null), - skills: Joi.array().items(Joi.string().uuid()), -- isApplicationPageActive: Joi.boolean() -+ isApplicationPageActive: Joi.boolean(), -+ roleIds: Joi.array().items(Joi.string().uuid().required()).allow(null) - }).required() - }).required() - -@@ -276,7 +307,8 @@ fullyUpdateJob.schema = Joi.object().keys({ - workload: Joi.workload().allow(null).default(null), - skills: Joi.array().items(Joi.string().uuid()).required(), - status: Joi.jobStatus().default('sourcing'), -- isApplicationPageActive: Joi.boolean() -+ isApplicationPageActive: Joi.boolean(), -+ roleIds: Joi.array().items(Joi.string().uuid().required()).default(null) - }).required() - }).required() - -@@ -444,9 +476,9 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } - [Op.like]: `%${criteria.title}%` - } - } -- if (criteria.skills) { -+ if (criteria.skill) { - filter.skills = { -- [Op.contains]: [criteria.skills] -+ [Op.contains]: [criteria.skill] - } - } - const jobs = await Job.findAll({ -diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js -index f5c4020..fd3d777 100644 ---- a/src/services/ResourceBookingService.js -+++ b/src/services/ResourceBookingService.js -@@ -1,3 +1,4 @@ -+/* eslint-disable no-unreachable */ - /** - * This service provides operations of ResourceBooking. - */ -diff --git a/src/services/RoleService.js b/src/services/RoleService.js -new file mode 100644 -index 0000000..19006f6 ---- /dev/null -+++ b/src/services/RoleService.js -@@ -0,0 +1,305 @@ -+/** -+ * This service provides operations of Roles. -+ */ -+ -+const _ = require('lodash') -+const config = require('config') -+const Joi = require('joi') -+const { Op } = require('sequelize') -+const uuid = require('uuid') -+const helper = require('../common/helper') -+const logger = require('../common/logger') -+const errors = require('../common/errors') -+const models = require('../models') -+ -+const Role = models.Role -+const esClient = helper.getESClient() -+ -+/** -+ * Check user permission for deleting, creating or updating role. -+ * @param {Object} currentUser the user who perform this operation. -+ * @returns {undefined} -+ */ -+async function _checkUserPermissionForWriteDeleteRole (currentUser) { -+ if (!currentUser.hasManagePermission && !currentUser.isMachine) { -+ throw new errors.ForbiddenError('You are not allowed to perform this action!') -+ } -+} -+ -+/** -+ * Cleans and validates skill names using skills service -+ * @param {Array} skills array of skill names to validate -+ * @returns {undefined} -+ */ -+async function _cleanAndValidateSkillNames (skills) { -+ // remove duplicates, leading and trailing whitespaces, remove empties and convert to lowercase. -+ const cleanedSkills = _.uniq(_.filter(_.map(skills, skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill))) -+ if (cleanedSkills.length > 0) { -+ // search skills if they are exists -+ const { result } = await helper.getTopcoderSkills({ name: _.join(cleanedSkills, ',') }) -+ const skillNames = _.map(result, 'name') -+ // find skills that not valid -+ const unValidSkills = _.differenceWith(cleanedSkills, skillNames, (a, b) => _.toLower(a) === _.toLower(b)) -+ if (unValidSkills.length > 0) { -+ throw new errors.BadRequestError(`skills: "${unValidSkills}" are not valid`) -+ } -+ return cleanedSkills -+ } else { -+ return null -+ } -+} -+ -+/** -+ * Check user permission for deleting, creating or updating role. -+ * @param {Object} currentUser the user who perform this operation. -+ * @returns {undefined} -+ */ -+async function _checkIfSameNamedRoleExists (roleName) { -+ // We can't create another Role with the same name -+ const role = await Role.findOne({ -+ where: { -+ name: { [Op.iLike]: roleName } -+ }, -+ raw: true -+ }) -+ if (role) { -+ throw new errors.BadRequestError(`Role: "${role.name}" is already exists.`) -+ } -+} -+ -+/** -+ * Get role by id -+ * @param {Object} currentUser the user who perform this operation. -+ * @param {String} id the role id -+ * @param {Boolean} fromDb flag if query db for data or not -+ * @returns {Object} the role -+ */ -+async function getRole (currentUser, id, fromDb = false) { -+ if (!fromDb) { -+ try { -+ const role = await esClient.get({ -+ index: config.esConfig.ES_INDEX_ROLE, -+ id -+ }) -+ return { id: role.body._id, ...role.body._source } -+ } catch (err) { -+ if (helper.isDocumentMissingException(err)) { -+ throw new errors.NotFoundError(`id: ${id} "Role" not found`) -+ } -+ } -+ } -+ logger.info({ component: 'RoleService', context: 'getRole', message: 'try to query db for data' }) -+ const role = await Role.findById(id) -+ -+ return role.toJSON() -+} -+ -+getRole.schema = Joi.object().keys({ -+ currentUser: Joi.object().required(), -+ id: Joi.string().uuid().required(), -+ fromDb: Joi.boolean() -+}).required() -+ -+/** -+ * Create role -+ * @param {Object} currentUser the user who perform this operation -+ * @param {Object} role the role to be created -+ * @returns {Object} the created role -+ */ -+async function createRole (currentUser, role) { -+ // check permission -+ await _checkUserPermissionForWriteDeleteRole(currentUser) -+ // check if another Role with the same name exists. -+ await _checkIfSameNamedRoleExists(role.name) -+ // clean and validate skill names -+ if (role.listOfSkills) { -+ role.listOfSkills = await _cleanAndValidateSkillNames(role.listOfSkills) -+ } -+ -+ role.id = uuid.v4() -+ role.createdBy = await helper.getUserId(currentUser.userId) -+ -+ const created = await Role.create(role) -+ -+ await helper.postEvent(config.TAAS_ROLE_CREATE_TOPIC, created.toJSON()) -+ return created.toJSON() -+} -+ -+createRole.schema = Joi.object().keys({ -+ currentUser: Joi.object().required(), -+ role: Joi.object().keys({ -+ name: Joi.string().max(50).required(), -+ description: Joi.string().max(1000), -+ listOfSkills: Joi.array().items(Joi.string().max(50).required()), -+ rates: Joi.array().items(Joi.object().keys({ -+ global: Joi.smallint().required(), -+ inCountry: Joi.smallint().required(), -+ offShore: Joi.smallint().required(), -+ rate30Global: Joi.smallint(), -+ rate30InCountry: Joi.smallint(), -+ rate30OffShore: Joi.smallint(), -+ rate20Global: Joi.smallint(), -+ rate20InCountry: Joi.smallint(), -+ rate20OffShore: Joi.smallint() -+ }).required()).required(), -+ numberOfMembers: Joi.number(), -+ numberOfMembersAvailable: Joi.smallint(), -+ imageUrl: Joi.string().uri().max(255), -+ timeToCandidate: Joi.smallint(), -+ timeToInterview: Joi.smallint() -+ }).required() -+}).required() -+ -+/** -+ * Partially Update role -+ * @param {Object} currentUser the user who perform this operation -+ * @param {String} id the role id -+ * @param {Object} data the data to be updated -+ * @returns {Object} the updated role -+ */ -+async function updateRole (currentUser, id, data) { -+ // check permission -+ await _checkUserPermissionForWriteDeleteRole(currentUser) -+ -+ const role = await Role.findById(id) -+ const oldValue = role.toJSON() -+ // if name is changed, check if another Role with the same name exists. -+ if (data.name && data.name.toLowerCase() !== role.dataValues.name.toLowerCase()) { -+ await _checkIfSameNamedRoleExists(data.name) -+ } -+ // clean and validate skill names -+ if (data.listOfSkills) { -+ data.listOfSkills = await _cleanAndValidateSkillNames(data.listOfSkills) -+ } -+ -+ data.updatedBy = await helper.getUserId(currentUser.userId) -+ const updated = await role.update(data) -+ -+ await helper.postEvent(config.TAAS_ROLE_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) -+ return updated.toJSON() -+} -+ -+updateRole.schema = Joi.object().keys({ -+ currentUser: Joi.object().required(), -+ id: Joi.string().uuid().required(), -+ data: Joi.object().keys({ -+ name: Joi.string().max(50), -+ description: Joi.string().max(1000).allow(null), -+ listOfSkills: Joi.array().items(Joi.string().max(50).required()).allow(null), -+ rates: Joi.array().items(Joi.object().keys({ -+ global: Joi.smallint().required(), -+ inCountry: Joi.smallint().required(), -+ offShore: Joi.smallint().required(), -+ rate30Global: Joi.smallint(), -+ rate30InCountry: Joi.smallint(), -+ rate30OffShore: Joi.smallint(), -+ rate20Global: Joi.smallint(), -+ rate20InCountry: Joi.smallint(), -+ rate20OffShore: Joi.smallint() -+ }).required()), -+ numberOfMembers: Joi.number().allow(null), -+ numberOfMembersAvailable: Joi.smallint().allow(null), -+ imageUrl: Joi.string().uri().max(255).allow(null), -+ timeToCandidate: Joi.smallint().allow(null), -+ timeToInterview: Joi.smallint().allow(null) -+ }).required() -+}).required() -+ -+/** -+ * Delete role by id -+ * @param {Object} currentUser the user who perform this operation -+ * @param {String} id the role id -+ */ -+async function deleteRole (currentUser, id) { -+ // check permission -+ await _checkUserPermissionForWriteDeleteRole(currentUser) -+ -+ const role = await Role.findById(id) -+ await role.destroy() -+ await helper.postEvent(config.TAAS_ROLE_DELETE_TOPIC, { id }) -+} -+ -+deleteRole.schema = Joi.object().keys({ -+ currentUser: Joi.object().required(), -+ id: Joi.string().uuid().required() -+}).required() -+ -+/** -+ * List roles -+ * @param {Object} currentUser the user who perform this operation. -+ * @param {Object} criteria the search criteria -+ * @returns {Object} the search result -+ */ -+async function searchRoles (currentUser, criteria) { -+ // clean skill names and convert into an array -+ criteria.skillsList = _.filter(_.map(_.split(_.trim(criteria.skillsList), ','), skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill)) -+ try { -+ const esQuery = { -+ index: config.get('esConfig.ES_INDEX_ROLE'), -+ body: { -+ query: { -+ bool: { -+ must: [] -+ } -+ }, -+ size: 10000, -+ sort: [{ name: { order: 'asc' } }] -+ } -+ } -+ // Apply skill name filters. listOfSkills array should include all skills provided in criteria. -+ _.each(criteria.skillsList, skill => { -+ esQuery.body.query.bool.must.push({ -+ term: { -+ listOfSkills: skill -+ } -+ }) -+ }) -+ // Apply name filter, allow partial match -+ if (criteria.keyword) { -+ esQuery.body.query.bool.must.push({ -+ wildcard: { -+ name: `*${criteria.keyword}*` -+ -+ } -+ }) -+ } -+ logger.debug({ component: 'RoleService', context: 'searchRoles', message: `Query: ${JSON.stringify(esQuery)}` }) -+ -+ const { body } = await esClient.search(esQuery) -+ return _.map(body.hits.hits, (hit) => _.assign(hit._source, { id: hit._id })) -+ } catch (err) { -+ logger.logFullError(err, { component: 'RoleService', context: 'searchRoles' }) -+ } -+ logger.info({ component: 'RoleService', context: 'searchRoles', message: 'fallback to DB query' }) -+ const filter = { [Op.and]: [] } -+ // Apply skill name filters. listOfSkills array should include all skills provided in criteria. -+ if (criteria.skillsList) { -+ filter[Op.and].push({ listOfSkills: { [Op.contains]: criteria.skillsList } }) -+ } -+ // Apply name filter, allow partial match and ignore case -+ if (criteria.keyword) { -+ filter[Op.and].push({ name: { [Op.iLike]: `%${criteria.keyword}%` } }) -+ } -+ const queryCriteria = { -+ where: filter, -+ order: [['name', 'asc']] -+ } -+ const roles = await Role.findAll(queryCriteria) -+ return roles -+} -+ -+searchRoles.schema = Joi.object().keys({ -+ currentUser: Joi.object().required(), -+ criteria: Joi.object().keys({ -+ skillsList: Joi.string(), -+ keyword: Joi.string() -+ }).required() -+}).required() -+ -+module.exports = { -+ getRole, -+ createRole, -+ updateRole, -+ deleteRole, -+ searchRoles -+} -diff --git a/src/services/TeamService.js b/src/services/TeamService.js -index 3f6dbfd..4052e94 100644 ---- a/src/services/TeamService.js -+++ b/src/services/TeamService.js -@@ -2,16 +2,16 @@ - * This service provides operations of Job. - */ - --const _ = require('lodash'); --const Joi = require('joi'); --const dateFNS = require('date-fns'); --const config = require('config'); --const emailTemplateConfig = require('../../config/email_template.config'); --const helper = require('../common/helper'); --const logger = require('../common/logger'); --const errors = require('../common/errors'); --const JobService = require('./JobService'); --const ResourceBookingService = require('./ResourceBookingService'); -+const _ = require('lodash') -+const Joi = require('joi') -+const dateFNS = require('date-fns') -+const config = require('config') -+const emailTemplateConfig = require('../../config/email_template.config') -+const helper = require('../common/helper') -+const logger = require('../common/logger') -+const errors = require('../common/errors') -+const JobService = require('./JobService') -+const ResourceBookingService = require('./ResourceBookingService') - - const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { - return { -@@ -20,9 +20,9 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { - from: template.from, - recipients: template.recipients, - cc: template.cc, -- sendgridTemplateId: template.sendgridTemplateId, -- }; --}); -+ sendgridTemplateId: template.sendgridTemplateId -+ } -+}) - - /** - * Function to get placed resource bookings with specific projectIds -@@ -30,14 +30,14 @@ const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { - * @param {Array} projectIds project ids - * @returns the request result - */ --async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { -- const criteria = { status: 'placed', projectIds }; -+async function _getPlacedResourceBookingsByProjectIds (currentUser, projectIds) { -+ const criteria = { status: 'placed', projectIds } - const { result } = await ResourceBookingService.searchResourceBookings( - currentUser, - criteria, - { returnAll: true } -- ); -- return result; -+ ) -+ return result - } - - /** -@@ -46,13 +46,13 @@ async function _getPlacedResourceBookingsByProjectIds(currentUser, projectIds) { - * @param {Array} projectIds project ids - * @returns the request result - */ --async function _getJobsByProjectIds(currentUser, projectIds) { -+async function _getJobsByProjectIds (currentUser, projectIds) { - const { result } = await JobService.searchJobs( - currentUser, - { projectIds }, - { returnAll: true } -- ); -- return result; -+ ) -+ return result - } - - /** -@@ -61,26 +61,26 @@ async function _getJobsByProjectIds(currentUser, projectIds) { - * @param {Object} criteria the search criteria - * @returns {Object} the search result, contain total/page/perPage and result array - */ --async function searchTeams(currentUser, criteria) { -- const sort = `${criteria.sortBy} ${criteria.sortOrder}`; -+async function searchTeams (currentUser, criteria) { -+ const sort = `${criteria.sortBy} ${criteria.sortOrder}` - // Get projects from /v5/projects with searching criteria - const { - total, - page, - perPage, -- result: projects, -+ result: projects - } = await helper.getProjects(currentUser, { - page: criteria.page, - perPage: criteria.perPage, - name: criteria.name, -- sort, -- }); -+ sort -+ }) - return { - total, - page, - perPage, -- result: await getTeamDetail(currentUser, projects), -- }; -+ result: await getTeamDetail(currentUser, projects) -+ } - } - - searchTeams.schema = Joi.object() -@@ -107,13 +107,13 @@ searchTeams.schema = Joi.object() - then: Joi.forbidden().label( - 'sortOrder(with sortBy being `best match`)' - ), -- otherwise: Joi.string().valid('asc', 'desc').default('desc'), -+ otherwise: Joi.string().valid('asc', 'desc').default('desc') - }), -- name: Joi.string(), -+ name: Joi.string() - }) -- .required(), -+ .required() - }) -- .required(); -+ .required() - - /** - * Get team details -@@ -122,69 +122,69 @@ searchTeams.schema = Joi.object() - * @param {Object} isSearch the flag whether for search function - * @returns {Object} the search result - */ --async function getTeamDetail(currentUser, projects, isSearch = true) { -- const projectIds = _.map(projects, 'id'); -+async function getTeamDetail (currentUser, projects, isSearch = true) { -+ const projectIds = _.map(projects, 'id') - // Get all placed resourceBookings filtered by projectIds - const resourceBookings = await _getPlacedResourceBookingsByProjectIds( - currentUser, - projectIds -- ); -+ ) - // Get all jobs filtered by projectIds -- const jobs = await _getJobsByProjectIds(currentUser, projectIds); -+ const jobs = await _getJobsByProjectIds(currentUser, projectIds) - - // Get first week day and last week day -- const curr = new Date(); -- const firstDay = dateFNS.startOfWeek(curr); -- const lastDay = dateFNS.endOfWeek(curr); -+ const curr = new Date() -+ const firstDay = dateFNS.startOfWeek(curr) -+ const lastDay = dateFNS.endOfWeek(curr) - - logger.debug({ - component: 'TeamService', - context: 'getTeamDetail', -- message: `week started: ${firstDay}, week ended: ${lastDay}`, -- }); -+ message: `week started: ${firstDay}, week ended: ${lastDay}` -+ }) - -- const result = []; -+ const result = [] - for (const project of projects) { -- const rbs = _.filter(resourceBookings, { projectId: project.id }); -- const res = _.clone(project); -- res.weeklyCost = 0; -- res.resources = []; -+ const rbs = _.filter(resourceBookings, { projectId: project.id }) -+ const res = _.clone(project) -+ res.weeklyCost = 0 -+ res.resources = [] - - if (rbs && rbs.length > 0) { - // Get minimal start date and maximal end date -- const startDates = []; -- const endDates = []; -+ const startDates = [] -+ const endDates = [] - for (const rbsItem of rbs) { - if (rbsItem.startDate) { -- startDates.push(new Date(rbsItem.startDate)); -+ startDates.push(new Date(rbsItem.startDate)) - } - if (rbsItem.endDate) { -- endDates.push(new Date(rbsItem.endDate)); -+ endDates.push(new Date(rbsItem.endDate)) - } - } - - if (startDates && startDates.length > 0) { -- res.startDate = _.min(startDates); -+ res.startDate = _.min(startDates) - } - if (endDates && endDates.length > 0) { -- res.endDate = _.max(endDates); -+ res.endDate = _.max(endDates) - } - - // Count weekly rate - for (const item of rbs) { - // ignore any resourceBooking that has customerRate missed - if (!item.customerRate) { -- continue; -+ continue - } -- const startDate = new Date(item.startDate); -- const endDate = new Date(item.endDate); -+ const startDate = new Date(item.startDate) -+ const endDate = new Date(item.endDate) - - // normally startDate is smaller than endDate for a resourceBooking so not check if startDate < endDate - if ( - (!item.startDate || startDate < lastDay) && - (!item.endDate || endDate > firstDay) - ) { -- res.weeklyCost += item.customerRate; -+ res.weeklyCost += item.customerRate - } - } - -@@ -194,48 +194,48 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { - const resource = { - id: rb.id, - userId: user.id, -- ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']), -- }; -+ ..._.pick(user, ['handle', 'firstName', 'lastName', 'skills']) -+ } - // If call function is not search, add jobId field - if (!isSearch) { -- resource.jobId = rb.jobId; -- resource.customerRate = rb.customerRate; -- resource.startDate = rb.startDate; -- resource.endDate = rb.endDate; -+ resource.jobId = rb.jobId -+ resource.customerRate = rb.customerRate -+ resource.startDate = rb.startDate -+ resource.endDate = rb.endDate - } -- return resource; -- }); -+ return resource -+ }) - }) -- ); -+ ) - if (resourceInfos && resourceInfos.length > 0) { -- res.resources = resourceInfos; -+ res.resources = resourceInfos - -- const userHandles = _.map(resourceInfos, 'handle'); -+ const userHandles = _.map(resourceInfos, 'handle') - // Get user photo from /v5/members -- const members = await helper.getMembers(userHandles); -+ const members = await helper.getMembers(userHandles) - - for (const item of res.resources) { - const findMember = _.find(members, { -- handleLower: item.handle.toLowerCase(), -- }); -+ handleLower: item.handle.toLowerCase() -+ }) - if (findMember && findMember.photoURL) { -- item.photo_url = findMember.photoURL; -+ item.photo_url = findMember.photoURL - } - } - } - } - -- const jobsTmp = _.filter(jobs, { projectId: project.id }); -+ const jobsTmp = _.filter(jobs, { projectId: project.id }) - if (jobsTmp && jobsTmp.length > 0) { - if (isSearch) { - // Count total positions -- res.totalPositions = 0; -+ res.totalPositions = 0 - for (const item of jobsTmp) { - // only sum numPositions of jobs whose status is NOT cancelled or closed - if (['cancelled', 'closed'].includes(item.status)) { -- continue; -+ continue - } -- res.totalPositions += item.numPositions; -+ res.totalPositions += item.numPositions - } - } else { - res.jobs = _.map(jobsTmp, (job) => { -@@ -249,15 +249,15 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { - 'skills', - 'customerRate', - 'status', -- 'title', -- ]); -- }); -+ 'title' -+ ]) -+ }) - } - } -- result.push(res); -+ result.push(res) - } - -- return result; -+ return result - } - - /** -@@ -266,35 +266,35 @@ async function getTeamDetail(currentUser, projects, isSearch = true) { - * @param {String} id the job id - * @returns {Object} the team - */ --async function getTeam(currentUser, id) { -- const project = await helper.getProjectById(currentUser, id); -- const result = await getTeamDetail(currentUser, [project], false); -- const teamDetail = result[0]; -+async function getTeam (currentUser, id) { -+ const project = await helper.getProjectById(currentUser, id) -+ const result = await getTeamDetail(currentUser, [project], false) -+ const teamDetail = result[0] - - // add job skills for result -- let jobSkills = []; -+ let jobSkills = [] - if (teamDetail && teamDetail.jobs) { - for (const job of teamDetail.jobs) { - if (job.skills) { -- const usersPromises = []; -+ const usersPromises = [] - _.map(job.skills, (skillId) => { -- usersPromises.push(helper.getSkillById(skillId)); -- }); -- jobSkills = await Promise.all(usersPromises); -- job.skills = jobSkills; -+ usersPromises.push(helper.getSkillById(skillId)) -+ }) -+ jobSkills = await Promise.all(usersPromises) -+ job.skills = jobSkills - } - } - } - -- return teamDetail; -+ return teamDetail - } - - getTeam.schema = Joi.object() - .keys({ - currentUser: Joi.object().required(), -- id: Joi.number().integer().required(), -+ id: Joi.number().integer().required() - }) -- .required(); -+ .required() - - /** - * Get team job with id -@@ -303,25 +303,25 @@ getTeam.schema = Joi.object() - * @param {String} jobId the job id - * @returns the team job - */ --async function getTeamJob(currentUser, id, jobId) { -- const project = await helper.getProjectById(currentUser, id); -- const jobs = await _getJobsByProjectIds(currentUser, [project.id]); -- const job = _.find(jobs, { id: jobId }); -+async function getTeamJob (currentUser, id, jobId) { -+ const project = await helper.getProjectById(currentUser, id) -+ const jobs = await _getJobsByProjectIds(currentUser, [project.id]) -+ const job = _.find(jobs, { id: jobId }) - - if (!job) { - throw new errors.NotFoundError( - `id: ${jobId} "Job" with Team id ${id} doesn't exist` -- ); -+ ) - } - const result = { - id: job.id, -- title: job.title, -- }; -+ title: job.title -+ } - - if (job.skills) { - result.skills = await Promise.all( - _.map(job.skills, (skillId) => helper.getSkillById(skillId)) -- ); -+ ) - } - - // If the job has candidates, the following data for each candidate would be populated: -@@ -336,12 +336,12 @@ async function getTeamJob(currentUser, id, jobId) { - _.map(_.uniq(_.map(job.candidates, 'userId')), (userId) => - helper.getUserById(userId, true) - ) -- ); -- const userMap = _.groupBy(users, 'id'); -+ ) -+ const userMap = _.groupBy(users, 'id') - - // find photo URLs for users -- const members = await helper.getMembers(_.map(users, 'handle')); -- const photoURLMap = _.groupBy(members, 'handleLower'); -+ const members = await helper.getMembers(_.map(users, 'handle')) -+ const photoURLMap = _.groupBy(members, 'handleLower') - - result.candidates = _.map(job.candidates, (candidate) => { - const candidateData = _.pick(candidate, [ -@@ -349,33 +349,33 @@ async function getTeamJob(currentUser, id, jobId) { - 'resume', - 'userId', - 'interviews', -- 'id', -- ]); -- const userData = userMap[candidate.userId][0]; -+ 'id' -+ ]) -+ const userData = userMap[candidate.userId][0] - // attach user data to the candidate - Object.assign( - candidateData, - _.pick(userData, ['handle', 'firstName', 'lastName', 'skills']) -- ); -+ ) - // attach photo URL to the candidate -- const handleLower = userData.handle.toLowerCase(); -+ const handleLower = userData.handle.toLowerCase() - if (photoURLMap[handleLower]) { -- candidateData.photo_url = photoURLMap[handleLower][0].photoURL; -+ candidateData.photo_url = photoURLMap[handleLower][0].photoURL - } -- return candidateData; -- }); -+ return candidateData -+ }) - } - -- return result; -+ return result - } - - getTeamJob.schema = Joi.object() - .keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required(), -- jobId: Joi.string().guid().required(), -+ jobId: Joi.string().guid().required() - }) -- .required(); -+ .required() - - /** - * Send email through a particular template -@@ -383,21 +383,21 @@ getTeamJob.schema = Joi.object() - * @param {Object} data the email object - * @returns {undefined} - */ --async function sendEmail(currentUser, data) { -- const template = emailTemplates[data.template]; -- const dataCC = data.cc || []; -- const templateCC = template.cc || []; -- const dataRecipients = data.recipients || []; -- const templateRecipients = template.recipients || []; -+async function sendEmail (currentUser, data) { -+ const template = emailTemplates[data.template] -+ const dataCC = data.cc || [] -+ const templateCC = template.cc || [] -+ const dataRecipients = data.recipients || [] -+ const templateRecipients = template.recipients || [] - const subjectBody = { - subject: data.subject || template.subject, -- body: data.body || template.body, -- }; -+ body: data.body || template.body -+ } - for (const key in subjectBody) { - subjectBody[key] = await helper.substituteStringByObject( - subjectBody[key], - data.data -- ); -+ ) - } - const emailData = { - // override template if coming data already have the 'from' address -@@ -407,9 +407,9 @@ async function sendEmail(currentUser, data) { - cc: _.uniq([...dataCC, ...templateCC]), - data: { ...data.data, ...subjectBody }, - sendgrid_template_id: template.sendgridTemplateId, -- version: 'v3', -- }; -- await helper.postEvent(config.EMAIL_TOPIC, emailData); -+ version: 'v3' -+ } -+ await helper.postEvent(config.EMAIL_TOPIC, emailData) - } - - sendEmail.schema = Joi.object() -@@ -423,11 +423,11 @@ sendEmail.schema = Joi.object() - data: Joi.object().required(), - from: Joi.string().email(), - recipients: Joi.array().items(Joi.string().email()).allow(null), -- cc: Joi.array().items(Joi.string().email()).allow(null), -+ cc: Joi.array().items(Joi.string().email()).allow(null) - }) -- .required(), -+ .required() - }) -- .required(); -+ .required() - - /** - * Add a member to a team as customer. -@@ -437,25 +437,25 @@ sendEmail.schema = Joi.object() - * @param {String} fields the fields to be returned - * @returns {Object} the member added - */ --async function _addMemberToProjectAsCustomer(projectId, userId, fields) { -+async function _addMemberToProjectAsCustomer (projectId, userId, fields) { - try { - const member = await helper.createProjectMember( - projectId, - { userId: userId, role: 'customer' }, - { fields } -- ); -- return member; -+ ) -+ return member - } catch (err) { -- err.message = _.get(err, 'response.body.message') || err.message; -+ err.message = _.get(err, 'response.body.message') || err.message - if (err.message && err.message.includes('User already registered')) { -- throw new Error('User is already added'); -+ throw new Error('User is already added') - } - logger.error({ - component: 'TeamService', - context: '_addMemberToProjectAsCustomer', -- message: err.message, -- }); -- throw err; -+ message: err.message -+ }) -+ throw err - } - } - -@@ -467,16 +467,16 @@ async function _addMemberToProjectAsCustomer(projectId, userId, fields) { - * @param {Object} data the object including members with handle/email to be added - * @returns {Object} the success/failed added members - */ --async function addMembers(currentUser, id, criteria, data) { -- await helper.getProjectById(currentUser, id); // check whether the user can access the project -+async function addMembers (currentUser, id, criteria, data) { -+ await helper.getProjectById(currentUser, id) // check whether the user can access the project - - const result = { - success: [], -- failed: [], -- }; -+ failed: [] -+ } - -- const handles = data.handles || []; -- const emails = data.emails || []; -+ const handles = data.handles || [] -+ const emails = data.emails || [] - - const handleMembers = await helper - .getMemberDetailsByHandles(handles) -@@ -484,9 +484,9 @@ async function addMembers(currentUser, id, criteria, data) { - _.map(members, (member) => ({ - ...member, - // populate members with lower-cased handle for case insensitive search -- handleLowerCase: member.handle.toLowerCase(), -+ handleLowerCase: member.handle.toLowerCase() - })) -- ); -+ ) - - const emailMembers = await helper - .getMemberDetailsByEmails(emails) -@@ -494,20 +494,20 @@ async function addMembers(currentUser, id, criteria, data) { - _.map(members, (member) => ({ - ...member, - // populate members with lower-cased email for case insensitive search -- emailLowerCase: member.email.toLowerCase(), -+ emailLowerCase: member.email.toLowerCase() - })) -- ); -+ ) - - await Promise.all([ - Promise.all( - handles.map((handle) => { - const memberDetails = _.find(handleMembers, { -- handleLowerCase: handle.toLowerCase(), -- }); -+ handleLowerCase: handle.toLowerCase() -+ }) - - if (!memberDetails) { -- result.failed.push({ error: "User doesn't exist", handle }); -- return; -+ result.failed.push({ error: "User doesn't exist", handle }) -+ return - } - - return _addMemberToProjectAsCustomer( -@@ -517,23 +517,23 @@ async function addMembers(currentUser, id, criteria, data) { - ) - .then((member) => { - // note, that we return `handle` in the same case it was in request -- result.success.push({ ...member, handle }); -+ result.success.push({ ...member, handle }) - }) - .catch((err) => { -- result.failed.push({ error: err.message, handle }); -- }); -+ result.failed.push({ error: err.message, handle }) -+ }) - }) - ), - - Promise.all( - emails.map((email) => { - const memberDetails = _.find(emailMembers, { -- emailLowerCase: email.toLowerCase(), -- }); -+ emailLowerCase: email.toLowerCase() -+ }) - - if (!memberDetails) { -- result.failed.push({ error: "User doesn't exist", email }); -- return; -+ result.failed.push({ error: "User doesn't exist", email }) -+ return - } - - return _addMemberToProjectAsCustomer( -@@ -543,16 +543,16 @@ async function addMembers(currentUser, id, criteria, data) { - ) - .then((member) => { - // note, that we return `email` in the same case it was in request -- result.success.push({ ...member, email }); -+ result.success.push({ ...member, email }) - }) - .catch((err) => { -- result.failed.push({ error: err.message, email }); -- }); -+ result.failed.push({ error: err.message, email }) -+ }) - }) -- ), -- ]); -+ ) -+ ]) - -- return result; -+ return result - } - - addMembers.schema = Joi.object() -@@ -561,18 +561,18 @@ addMembers.schema = Joi.object() - id: Joi.number().integer().required(), - criteria: Joi.object() - .keys({ -- fields: Joi.string(), -+ fields: Joi.string() - }) - .required(), - data: Joi.object() - .keys({ - handles: Joi.array().items(Joi.string()), -- emails: Joi.array().items(Joi.string().email()), -+ emails: Joi.array().items(Joi.string().email()) - }) - .or('handles', 'emails') -- .required(), -+ .required() - }) -- .required(); -+ .required() - - /** - * Search members in a team. -@@ -583,9 +583,9 @@ addMembers.schema = Joi.object() - * @params {Object} criteria the search criteria - * @returns {Object} the search result - */ --async function searchMembers(currentUser, id, criteria) { -- const result = await helper.listProjectMembers(currentUser, id, criteria); -- return { result }; -+async function searchMembers (currentUser, id, criteria) { -+ const result = await helper.listProjectMembers(currentUser, id, criteria) -+ return { result } - } - - searchMembers.schema = Joi.object() -@@ -595,11 +595,11 @@ searchMembers.schema = Joi.object() - criteria: Joi.object() - .keys({ - role: Joi.string(), -- fields: Joi.string(), -+ fields: Joi.string() - }) -- .required(), -+ .required() - }) -- .required(); -+ .required() - - /** - * Search member invites for a team. -@@ -610,13 +610,13 @@ searchMembers.schema = Joi.object() - * @params {Object} criteria the search criteria - * @returns {Object} the search result - */ --async function searchInvites(currentUser, id, criteria) { -+async function searchInvites (currentUser, id, criteria) { - const result = await helper.listProjectMemberInvites( - currentUser, - id, - criteria -- ); -- return { result }; -+ ) -+ return { result } - } - - searchInvites.schema = Joi.object() -@@ -625,11 +625,11 @@ searchInvites.schema = Joi.object() - id: Joi.number().integer().required(), - criteria: Joi.object() - .keys({ -- fields: Joi.string(), -+ fields: Joi.string() - }) -- .required(), -+ .required() - }) -- .required(); -+ .required() - - /** - * Remove a member from a team. -@@ -640,17 +640,17 @@ searchInvites.schema = Joi.object() - * @param {String} projectMemberId the id of the project member - * @returns {undefined} - */ --async function deleteMember(currentUser, id, projectMemberId) { -- await helper.deleteProjectMember(currentUser, id, projectMemberId); -+async function deleteMember (currentUser, id, projectMemberId) { -+ await helper.deleteProjectMember(currentUser, id, projectMemberId) - } - - deleteMember.schema = Joi.object() - .keys({ - currentUser: Joi.object().required(), - id: Joi.number().integer().required(), -- projectMemberId: Joi.number().integer().required(), -+ projectMemberId: Joi.number().integer().required() - }) -- .required(); -+ .required() - - /** - * Return details about the current user. -@@ -659,31 +659,31 @@ deleteMember.schema = Joi.object() - * @params {Object} criteria the search criteria - * @returns {Object} the user data for current user - */ --async function getMe(currentUser) { -- return helper.getUserByExternalId(currentUser.userId); -+async function getMe (currentUser) { -+ return helper.getUserByExternalId(currentUser.userId) - } - - getMe.schema = Joi.object() - .keys({ -- currentUser: Joi.object().required(), -+ currentUser: Joi.object().required() - }) -- .required(); -+ .required() - - /** - * @param {Object} currentUser the user performing the operation. - * @param {Object} data project data - * @returns {Object} the created project - */ --async function createProj(currentUser, data) { -- return helper.createProject(currentUser, data); -+async function createProj (currentUser, data) { -+ return helper.createProject(currentUser, data) - } - - createProj.schema = Joi.object() - .keys({ - currentUser: Joi.object().required(), -- data: Joi.object().required(), -+ data: Joi.object().required() - }) -- .required(); -+ .required() - - module.exports = { - searchTeams, -@@ -695,5 +695,5 @@ module.exports = { - searchInvites, - deleteMember, - getMe, -- createProj, --}; -+ createProj -+} --- -2.29.1.windows.1 - From 28270acb1cbcd2dbfeaf042e82908ff3579d76ac Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Tue, 1 Jun 2021 12:12:11 +0530 Subject: [PATCH 24/86] fix: added allowNull: true for new cols --- ...2021-05-28-add-fields-to-job-and-job-candidate.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/migrations/2021-05-28-add-fields-to-job-and-job-candidate.js b/migrations/2021-05-28-add-fields-to-job-and-job-candidate.js index 472a25a8..93b427b6 100644 --- a/migrations/2021-05-28-add-fields-to-job-and-job-candidate.js +++ b/migrations/2021-05-28-add-fields-to-job-and-job-candidate.js @@ -10,22 +10,22 @@ module.exports = { const transaction = await queryInterface.sequelize.transaction() try { await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'min_salary', - { type: Sequelize.INTEGER, allowNull: false }, + { type: Sequelize.INTEGER, allowNull: true }, { transaction }) await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'max_salary', - { type: Sequelize.INTEGER, allowNull: false }, + { type: Sequelize.INTEGER, allowNull: true }, { transaction }) await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'hours_per_week', - { type: Sequelize.INTEGER, allowNull: false }, + { type: Sequelize.INTEGER, allowNull: true }, { transaction }) await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'job_location', - { type: Sequelize.STRING(255), allowNull: false }, + { type: Sequelize.STRING(255), allowNull: true }, { transaction }) await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'job_timezone', - { type: Sequelize.STRING(128), allowNull: false }, + { type: Sequelize.STRING(128), allowNull: true }, { transaction }) await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'currency', - { type: Sequelize.STRING(30), allowNull: false }, + { type: Sequelize.STRING(30), allowNull: true }, { transaction }) await queryInterface.addColumn({ tableName: 'job_candidates', schema: config.DB_SCHEMA_NAME }, 'remark', { type: Sequelize.STRING(255) }, From 99c35db0203ae4a9e8fdfff13e73b3bde54ca0c9 Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Tue, 1 Jun 2021 12:50:12 +0530 Subject: [PATCH 25/86] fix: fixed syntax --- src/services/JobService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/JobService.js b/src/services/JobService.js index a30328ca..2d06fd56 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -208,7 +208,7 @@ createJob.schema = Joi.object().keys({ hoursPerWeek: Joi.number().integer().allow(null), jobLocation: Joi.string().allow(null), jobTimezone: Joi.string().allow(null), - currency: Joi.string().allow(null) + currency: Joi.string().allow(null), roleIds: Joi.array().items(Joi.string().uuid().required()) }).required() }).required() From d1ac310e2b208780d6e677fe22e413757436d3ab Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Tue, 1 Jun 2021 12:56:51 +0530 Subject: [PATCH 26/86] fix: fixed syntax comma --- src/services/JobService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/JobService.js b/src/services/JobService.js index 2d06fd56..b310d4ab 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -325,7 +325,7 @@ fullyUpdateJob.schema = Joi.object().keys({ hoursPerWeek: Joi.number().integer().allow(null), jobLocation: Joi.string().allow(null), jobTimezone: Joi.string().allow(null), - currency: Joi.string().allow(null) + currency: Joi.string().allow(null), roleIds: Joi.array().items(Joi.string().uuid().required()).default(null) }).required() }).required() From 42c2ac35c2cee34369179a925ef1d51961e3b6a6 Mon Sep 17 00:00:00 2001 From: yoution Date: Tue, 1 Jun 2021 15:28:15 +0800 Subject: [PATCH 27/86] Role & Skills Intake - Job Description Module --- ...coder-bookings-api.postman_collection.json | 42 +++++++++++- docs/swagger.yaml | 65 +++++++++++++++++++ src/common/helper.js | 24 +++++++ src/controllers/TeamController.js | 11 ++++ src/routes/TeamRoutes.js | 8 +++ src/services/TeamService.js | 24 +++++++ 6 files changed, 172 insertions(+), 2 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 74155753..0059a126 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", + "_postman_id": "b25dc4fc-ac96-49a5-b8b0-7700c625d4d0", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -17647,6 +17647,44 @@ }, "response": [] }, + { + "name": "POST /taas-teams/getSkillsByJobDescription", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"description\": \"nodejs react c++ hello\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "getSkillsByJobDescription" + ] + } + }, + "response": [] + }, { "name": "POST /taas-teams/email - member-issue-report", "request": { @@ -27007,4 +27045,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1584f5cf..4359781a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3170,6 +3170,54 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + + /taas-teams/getSkillsByJobDescription: + post: + tags: + - Teams + description: | + Get skill list by Job Description + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TeamJobDescriptionRequestBody" + responses: + "200": + description: OK + content: + application/json: + schema: + type : array + items : { + $ref: "#/components/schemas/SkillItem" + } + "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/email: post: tags: @@ -3282,6 +3330,15 @@ components: scheme: bearer bearerFormat: JWT schemas: + SkillItem: + properties: + tag: + type: string + type: + type: string + source: + type: string + Job: required: - id @@ -4666,6 +4723,14 @@ components: type: array items: $ref: "#/components/schemas/Skill" + TeamJobDescriptionRequestBody: + type: object + properties: + description: + type: string + description: "job description" + example: "nodejs and java" + TeamEmailRequestBody: type: object properties: diff --git a/src/common/helper.js b/src/common/helper.js index e68e5c58..64226267 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1691,6 +1691,29 @@ async function substituteStringByObject(string, object) { return string; } + +/** + * Get tags from tagging service + * @param {String} description The challenge description + * @returns {Array} array of tags + */ +async function getTags (description) { + const data = { text: description, extract_confidence: false} + const type = "emsi/internal_no_refresh" + const url = `${config.TC_API}/contest-tagging/${type}`; + const res = await request + .post(url) + .set('Accept', 'application/json') + .send(querystring.stringify(data)) + + localLogger.debug({ + context: 'getTags', + message: `response body: ${JSON.stringify(res.body)}`, + }); + return _.get(res, 'body'); +} + + /** * @param {Object} currentUser the user performing the action * @param {Object} data title of project and any other info @@ -1752,6 +1775,7 @@ module.exports = { getMemberDetailsByHandles, getMemberDetailsByHandle, getMemberDetailsByEmails, + getTags, createProjectMember, listProjectMembers, listProjectMemberInvites, diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index ca4f1bca..94b831c5 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -108,6 +108,16 @@ 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)); +} + /** * * @param req the request @@ -127,5 +137,6 @@ module.exports = { searchInvites, deleteMember, getMe, + getSkillsByJobDescription, createProj, }; diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index 9bbe25c6..35292b01 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -36,6 +36,14 @@ module.exports = { scopes: [constants.Scopes.READ_TAAS_TEAM], }, }, + '/taas-teams/getSkillsByJobDescription': { + post: { + controller: 'TeamController', + method: 'getSkillsByJobDescription', + auth: 'jwt', + scopes: [constants.Scopes.READ_TAAS_TEAM], + }, + }, '/taas-teams/:id': { get: { controller: 'TeamController', diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 3f6dbfd3..2ead4d74 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -669,6 +669,29 @@ getMe.schema = Joi.object() }) .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 + */ +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(); + + /** * @param {Object} currentUser the user performing the operation. * @param {Object} data project data @@ -695,5 +718,6 @@ module.exports = { searchInvites, deleteMember, getMe, + getSkillsByJobDescription, createProj, }; From 727d09e84c9528b2e26cf7f10cf4d123740ad9a1 Mon Sep 17 00:00:00 2001 From: urwithat Date: Tue, 1 Jun 2021 14:04:38 +0530 Subject: [PATCH 28/86] Changed the endpoint roles to taas-roles --- ...coder-bookings-api.postman_collection.json | 324 +++++++++--------- docs/swagger.yaml | 6 +- src/routes/RoleRoutes.js | 4 +- 3 files changed, 167 insertions(+), 167 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 46d0389f..6d718c51 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -17966,12 +17966,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18015,12 +18015,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18064,12 +18064,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18111,12 +18111,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18158,12 +18158,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18205,12 +18205,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18252,12 +18252,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18299,12 +18299,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18346,12 +18346,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18393,12 +18393,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18440,12 +18440,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18487,12 +18487,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18534,12 +18534,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18581,12 +18581,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18628,12 +18628,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18675,12 +18675,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18722,12 +18722,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18769,12 +18769,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18816,12 +18816,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18863,12 +18863,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18910,12 +18910,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -18957,12 +18957,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -19004,12 +19004,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -19045,12 +19045,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -19082,12 +19082,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-2}}?fromDb=true", + "raw": "{{URL}}/taas-roles/{{roleId-2}}?fromDb=true", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-2}}" ], "query": [ @@ -19125,12 +19125,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-3}}", + "raw": "{{URL}}/taas-roles/{{roleId-3}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-3}}" ] } @@ -19162,12 +19162,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-1}}?fromDb=true", + "raw": "{{URL}}/taas-roles/{{roleId-1}}?fromDb=true", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ], "query": [ @@ -19205,12 +19205,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-2}}", + "raw": "{{URL}}/taas-roles/{{roleId-2}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-2}}" ] } @@ -19244,12 +19244,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-2}}", + "raw": "{{URL}}/taas-roles/{{roleId-2}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-2}}" ] } @@ -19283,12 +19283,12 @@ } ], "url": { - "raw": "{{URL}}/roles/invalid", + "raw": "{{URL}}/taas-roles/invalid", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "invalid" ] } @@ -19322,12 +19322,12 @@ } ], "url": { - "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", + "raw": "{{URL}}/taas-roles/00000000-0000-0000-0000-000000000000", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "00000000-0000-0000-0000-000000000000" ] } @@ -19359,12 +19359,12 @@ } ], "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -19395,12 +19395,12 @@ } ], "url": { - "raw": "{{URL}}/roles?skillsList=dropwizard, nginx,, machine learning , FORce.com &keyword=ops e", + "raw": "{{URL}}/taas-roles?skillsList=dropwizard, nginx,, machine learning , FORce.com &keyword=ops e", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ], "query": [ { @@ -19441,12 +19441,12 @@ } ], "url": { - "raw": "{{URL}}/roles?skillsList=dataBase, ,Photoshop&keyword=sale", + "raw": "{{URL}}/taas-roles?skillsList=dataBase, ,Photoshop&keyword=sale", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ], "query": [ { @@ -19487,12 +19487,12 @@ } ], "url": { - "raw": "{{URL}}/roles?skillsList=DOCKER,.NET&keyword=dev", + "raw": "{{URL}}/taas-roles?skillsList=DOCKER,.NET&keyword=dev", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ], "query": [ { @@ -19533,12 +19533,12 @@ } ], "url": { - "raw": "{{URL}}/roles?keyword=dev", + "raw": "{{URL}}/taas-roles?keyword=dev", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ], "query": [ { @@ -19577,12 +19577,12 @@ } ], "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -19627,12 +19627,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -19673,12 +19673,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-2}}", + "raw": "{{URL}}/taas-roles/{{roleId-2}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-2}}" ] } @@ -19719,12 +19719,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-3}}", + "raw": "{{URL}}/taas-roles/{{roleId-3}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-3}}" ] } @@ -19767,12 +19767,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -19815,12 +19815,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -19863,12 +19863,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -19911,12 +19911,12 @@ } }, "url": { - "raw": "{{URL}}/roles/invalid", + "raw": "{{URL}}/taas-roles/invalid", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "invalid" ] } @@ -19959,12 +19959,12 @@ } }, "url": { - "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", + "raw": "{{URL}}/taas-roles/00000000-0000-0000-0000-000000000000", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "00000000-0000-0000-0000-000000000000" ] } @@ -20007,12 +20007,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20055,12 +20055,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20103,12 +20103,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20151,12 +20151,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20199,12 +20199,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20247,12 +20247,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20295,12 +20295,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20343,12 +20343,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20391,12 +20391,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20439,12 +20439,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20487,12 +20487,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20540,12 +20540,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20588,12 +20588,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20636,12 +20636,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20684,12 +20684,12 @@ } }, "url": { - "raw": "{{URL}}/roles/invalid", + "raw": "{{URL}}/taas-roles/invalid", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "invalid" ] } @@ -20732,12 +20732,12 @@ } }, "url": { - "raw": "{{URL}}/roles/00000000-0000-0000-0000-000000000000", + "raw": "{{URL}}/taas-roles/00000000-0000-0000-0000-000000000000", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "00000000-0000-0000-0000-000000000000" ] } @@ -20778,12 +20778,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -20824,12 +20824,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-2}}", + "raw": "{{URL}}/taas-roles/{{roleId-2}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-2}}" ] } @@ -20870,12 +20870,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-3}}", + "raw": "{{URL}}/taas-roles/{{roleId-3}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-3}}" ] } @@ -25532,12 +25532,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -25568,12 +25568,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -25605,12 +25605,12 @@ } ], "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -25650,12 +25650,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -25696,12 +25696,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -27995,12 +27995,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -28044,12 +28044,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -28080,12 +28080,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -28117,12 +28117,12 @@ } ], "url": { - "raw": "{{URL}}/roles?keyword=Dev", + "raw": "{{URL}}/taas-roles?keyword=Dev", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ], "query": [ { @@ -28170,12 +28170,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -28218,12 +28218,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -30509,12 +30509,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -30558,12 +30558,12 @@ } }, "url": { - "raw": "{{URL}}/roles", + "raw": "{{URL}}/taas-roles", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ] } }, @@ -30594,12 +30594,12 @@ } ], "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -30631,12 +30631,12 @@ } ], "url": { - "raw": "{{URL}}/roles?skillsList=Dropwizard, ,NGINX&keyword=Dev", + "raw": "{{URL}}/taas-roles?skillsList=Dropwizard, ,NGINX&keyword=Dev", "host": [ "{{URL}}" ], "path": [ - "roles" + "taas-roles" ], "query": [ { @@ -30688,12 +30688,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } @@ -30736,12 +30736,12 @@ } }, "url": { - "raw": "{{URL}}/roles/{{roleId-1}}", + "raw": "{{URL}}/taas-roles/{{roleId-1}}", "host": [ "{{URL}}" ], "path": [ - "roles", + "taas-roles", "{{roleId-1}}" ] } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 808653de..40b1d474 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3258,7 +3258,7 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /roles/new: + /taas-roles/new: post: tags: - Roles @@ -3304,7 +3304,7 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /roles: + /taas-roles: get: tags: - Roles @@ -3354,7 +3354,7 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /roles/{id}: + /taas-roles/{id}: get: tags: - Roles diff --git a/src/routes/RoleRoutes.js b/src/routes/RoleRoutes.js index 2fb6d55b..7230b593 100644 --- a/src/routes/RoleRoutes.js +++ b/src/routes/RoleRoutes.js @@ -4,7 +4,7 @@ const constants = require('../../app-constants') module.exports = { - '/roles': { + '/taas-roles': { post: { controller: 'RoleController', method: 'createRole', @@ -18,7 +18,7 @@ module.exports = { scopes: [constants.Scopes.READ_ROLE, constants.Scopes.ALL_ROLE] } }, - '/roles/:id': { + '/taas-roles/:id': { get: { controller: 'RoleController', method: 'getRole', From 71ae9ab9a5a37af4b1d1721c140935f8ea06f396 Mon Sep 17 00:00:00 2001 From: xxcxy Date: Wed, 2 Jun 2021 19:25:48 +0800 Subject: [PATCH 29/86] Payments - Batch Endpoints --- app-constants.js | 9 +- config/default.js | 3 +- data/demo-data.json | 36 +- ...coder-bookings-api.postman_collection.json | 320 ++++++++++++++---- docs/swagger.yaml | 174 +++++++++- ...-26-work-period-payment-table-migration.js | 38 +++ src/bootstrap.js | 19 +- .../WorkPeriodPaymentController.js | 13 +- src/models/WorkPeriodPayment.js | 10 +- src/routes/WorkPeriodPaymentRoutes.js | 8 + src/services/InterviewService.js | 4 +- src/services/ResourceBookingService.js | 1 - src/services/WorkPeriodPaymentService.js | 197 ++++++++--- test/unit/ResourceBookingService.test.js | 4 + test/unit/WorkPeriodPaymentService.test.js | 23 +- test/unit/common/WorkPeriodPaymentData.js | 5 +- 16 files changed, 672 insertions(+), 192 deletions(-) create mode 100644 migrations/2021-05-26-work-period-payment-table-migration.js diff --git a/app-constants.js b/app-constants.js index 534e46de..7433499a 100644 --- a/app-constants.js +++ b/app-constants.js @@ -76,9 +76,10 @@ const ChallengeStatus = { COMPLETED: 'Completed' } -const PaymentProcessingSwitch = { - ON: 'ON', - OFF: 'OFF' +const WorkPeriodPaymentStatus = { + COMPLETED: 'completed', + CANCELLED: 'cancelled', + SCHEDULED: 'scheduled' } module.exports = { @@ -87,5 +88,5 @@ module.exports = { Scopes, Interviews, ChallengeStatus, - PaymentProcessingSwitch + WorkPeriodPaymentStatus } diff --git a/config/default.js b/config/default.js index 2b5ca7ba..9b4d6e81 100644 --- a/config/default.js +++ b/config/default.js @@ -160,7 +160,6 @@ module.exports = { ROLE_ID_SUBMITTER: process.env.ROLE_ID_SUBMITTER || '732339e7-8e30-49d7-9198-cccf9451e221', TYPE_ID_TASK: process.env.TYPE_ID_TASK || 'ecd58c69-238f-43a4-a4bb-d172719b9f31', 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', + DEFAULT_TRACK_ID: process.env.DEFAULT_TRACK_ID || '9b6fc876-f4d9-4ccb-9dfd-419247628825' - PAYMENT_PROCESSING_SWITCH: process.env.PAYMENT_PROCESSING_SWITCH || 'OFF' } diff --git a/data/demo-data.json b/data/demo-data.json index e0733443..e7fbe36e 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -182,12 +182,12 @@ { "id": "077aa2ca-5b60-4ad9-a965-1b37e08a5046", "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", - "googleCalendarId": null, + "calendarEventId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Completed", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, @@ -211,12 +211,12 @@ { "id": "b1f7ba76-640f-47e2-9463-59e51b51ec60", "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": "dummyId", + "calendarEventId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Scheduling", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, @@ -226,12 +226,12 @@ { "id": "3144fa65-ea1a-4bec-81b0-7cb1c8845826", "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": null, + "calendarEventId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Completed", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, @@ -255,12 +255,12 @@ { "id": "976d23a9-5710-453f-99d9-f57a588bb610", "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", + "calendarEventId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 3, "startTimestamp": null, - "attendeesList": [ + "guestEmails": [ "attendee1@yopmail.com", "attendee2@yopmail.com" ], @@ -273,12 +273,12 @@ { "id": "a23e1bf2-1084-4cfe-a0d8-d83bc6fec655", "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", + "calendarEventId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, - "attendeesList": [ + "guestEmails": [ "attendee1@yopmail.com", "attendee2@yopmail.com" ], @@ -291,12 +291,12 @@ { "id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb", "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": null, + "calendarEventId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Completed", "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 a0518c50..4c1e6fca 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "58b277bb-0d1d-4bbf-919f-c5951ba0e1c0", + "_postman_id": "2252a54a-8d60-4855-acd1-51138f7edc70", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -14749,6 +14749,55 @@ }, "response": [] }, + { + "name": "create work period2", + "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(\"workPeriodId2\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-14\",\r\n \"endDate\": \"2021-03-20\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-periods", + "host": [ + "{{URL}}" + ], + "path": [ + "work-periods" + ] + } + }, + "response": [] + }, { "name": "create work period with m2m", "event": [ @@ -14830,7 +14879,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -14850,7 +14899,7 @@ "response": [] }, { - "name": "create work period payment with m2m create", + "name": "create multiple work period payments with boooking manager", "event": [ { "listen": "test", @@ -14858,10 +14907,6 @@ "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(\"workPeriodPaymentIdCreatedByM2M\", response.id);\r", - " }\r", "});" ], "type": "text/javascript" @@ -14874,12 +14919,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_m2m_create_work_period_payment}}" + "value": "Bearer {{token_bookingManager}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodIdCreatedByM2M}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "[{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600\r\n},{\r\n \"workPeriodId\": \"{{workPeriodId2}}\",\r\n \"amount\": 900\r\n}]", "options": { "raw": { "language": "json" @@ -14899,16 +14944,14 @@ "response": [] }, { - "name": "create work period payment with connect user", + "name": "create query work period payments with boooking manager", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -14921,12 +14964,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_connectUser}}" + "value": "Bearer {{token_bookingManager}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\"query\": { \"workPeriods.paymentStatus\": \"pending\" } }", "options": { "raw": { "language": "json" @@ -14934,28 +14977,31 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments", + "raw": "{{URL}}/work-period-payments/query", "host": [ "{{URL}}" ], "path": [ - "work-period-payments" + "work-period-payments", + "query" ] } }, "response": [] }, { - "name": "create work period payment with member", + "name": "create work period payment with m2m create", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "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(\"workPeriodPaymentIdCreatedByM2M\", response.id);\r", + " }\r", "});" ], "type": "text/javascript" @@ -14968,12 +15014,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_member}}" + "value": "Bearer {{token_m2m_create_work_period_payment}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodIdCreatedByM2M}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -14993,16 +15039,16 @@ "response": [] }, { - "name": "create work period payment with user id not exist", + "name": "create work period payment with connect user", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 400', function () {\r", - " pm.response.to.have.status(400);\r", + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"Bad Request\")\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", "});" ], "type": "text/javascript" @@ -15015,12 +15061,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_userId_not_exist}}" + "value": "Bearer {{token_connectUser}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -15040,16 +15086,16 @@ "response": [] }, { - "name": "create work period payment with invalid token", + "name": "create work period payment with member", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 401', function () {\r", - " pm.response.to.have.status(401);\r", + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", "});" ], "type": "text/javascript" @@ -15062,12 +15108,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer invalid_token" + "value": "Bearer {{token_member}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -15087,7 +15133,7 @@ "response": [] }, { - "name": "create work period payment with missing workPeriodId", + "name": "create work period payment with user id not exist", "event": [ { "listen": "test", @@ -15096,7 +15142,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(\"\\\"workPeriodPayment.workPeriodId\\\" is required\")\r", + " pm.expect(response.message).to.eq(\"Bad Request\")\r", "});" ], "type": "text/javascript" @@ -15109,12 +15155,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_userId_not_exist}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -15134,16 +15180,16 @@ "response": [] }, { - "name": "create work period payment with invalid workPeriodId 1", + "name": "create work period payment with invalid token", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 400', function () {\r", - " pm.response.to.have.status(400);\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(\"\\\"workPeriodPayment.workPeriodId\\\" must be a valid GUID\")\r", + " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", "});" ], "type": "text/javascript" @@ -15156,12 +15202,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer invalid_token" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"aaa-bb-c\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -15181,7 +15227,7 @@ "response": [] }, { - "name": "create work period payment with invalid workPeriodId 2", + "name": "create work period payment with missing workPeriodId", "event": [ { "listen": "test", @@ -15190,7 +15236,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(\"\\\"workPeriodPayment.workPeriodId\\\" must be a string\")\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.workPeriodId\\\" is required\")\r", "});" ], "type": "text/javascript" @@ -15208,7 +15254,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": 123,\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -15228,7 +15274,7 @@ "response": [] }, { - "name": "create work period payment with invalid amount 1", + "name": "create work period payment with invalid workPeriodId 1", "event": [ { "listen": "test", @@ -15237,7 +15283,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(\"\\\"workPeriodPayment.amount\\\" must be a number\")\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.workPeriodId\\\" must be a valid GUID\")\r", "});" ], "type": "text/javascript" @@ -15255,7 +15301,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": \"abc\",\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"aaa-bb-c\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -15275,7 +15321,7 @@ "response": [] }, { - "name": "create work period payment with invalid amount 2", + "name": "create work period payment with invalid workPeriodId 2", "event": [ { "listen": "test", @@ -15284,7 +15330,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(\"\\\"workPeriodPayment.amount\\\" must be greater than 0\")\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.workPeriodId\\\" must be a string\")\r", "});" ], "type": "text/javascript" @@ -15302,7 +15348,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 0,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": 123,\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -15322,7 +15368,7 @@ "response": [] }, { - "name": "create work period payment with invalid status 1", + "name": "create work period payment with invalid amount 1", "event": [ { "listen": "test", @@ -15331,7 +15377,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(\"\\\"workPeriodPayment.status\\\" must be one of [completed, cancelled]\")\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.amount\\\" must be a number\")\r", "});" ], "type": "text/javascript" @@ -15349,7 +15395,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 1200,\r\n \"status\": 123\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": \"abc\"\r\n}", "options": { "raw": { "language": "json" @@ -15369,7 +15415,7 @@ "response": [] }, { - "name": "create work period payment with invalid status 2", + "name": "create work period payment with invalid amount 2", "event": [ { "listen": "test", @@ -15378,7 +15424,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(\"\\\"workPeriodPayment.status\\\" must be one of [completed, cancelled]\")\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.amount\\\" must be greater than 0\")\r", "});" ], "type": "text/javascript" @@ -15396,7 +15442,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 1200,\r\n \"status\": \"invalid-status\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 0\r\n}", "options": { "raw": { "language": "json" @@ -22198,7 +22244,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_by_administrator}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -22217,6 +22263,52 @@ }, "response": [] }, + { + "name": "✔ create multiple work period payment 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": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\"query\": { \"workPeriods.paymentStatus\": \"pending\" } }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments/query", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments", + "query" + ] + } + }, + "response": [] + }, { "name": "✔ get work period payment with administrator", "event": [ @@ -24431,7 +24523,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_member}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -24450,6 +24542,54 @@ }, "response": [] }, + { + "name": "✘ create query work period payment with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member_tester1234}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\"query\": { \"workPeriods.paymentStatus\": \"pending\" } }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments/query", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments", + "query" + ] + } + }, + "response": [] + }, { "name": "✘ get work period payment with member", "event": [ @@ -26664,7 +26804,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_connect_manager}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId_created_for_connect_manager}}\",\r\n \"amount\": 600\r\n}", "options": { "raw": { "language": "json" @@ -26683,6 +26823,54 @@ }, "response": [] }, + { + "name": "✘ create query work period payment with connect manager", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connect_manager_pshahcopmanag2}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\"query\": { \"workPeriods.paymentStatus\": \"pending\" } }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments/query", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments", + "query" + ] + } + }, + "response": [] + }, { "name": "✘ get work period payment with connect manager", "event": [ @@ -26900,4 +27088,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a0b6064b..9382de52 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2280,14 +2280,24 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/WorkPeriodPaymentRequestBody" + oneOf: + - $ref: "#/components/schemas/WorkPeriodPaymentCreateRequestBody" + - type: array + items: + $ref: "#/components/schemas/WorkPeriodPaymentCreateRequestBody" responses: "200": description: OK content: application/json: schema: - $ref: "#/components/schemas/WorkPeriodPayment" + oneOf: + - $ref: "#/components/schemas/WorkPeriodPayment" + - type: array + items: + oneOf: + - $ref: "#/components/schemas/WorkPeriodPayment" + - $ref: "#/components/schemas/WorkPeriodPaymentCreatedError" "400": description: Bad request content: @@ -2380,7 +2390,7 @@ paths: required: false schema: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "cancelled"] description: The payment status. responses: "200": @@ -2444,6 +2454,59 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /work-period-payments/query: + post: + tags: + - WorkPeriodPayments + description: | + Create Multiple Work Period Payments for all the pages at once. + + **Authorization** Topcoder token with write Work period payment scope is allowed + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/WorkPeriodPaymentQueryCreateRequestBody" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/WorkPeriodPaymentQueryCreateResult" + "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" + /work-period-payments/{id}: get: tags: @@ -4211,7 +4274,7 @@ components: description: "The amount to be paid." status: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "cancelled"] description: "The payment status." billingAccountId: type: integer @@ -4233,6 +4296,28 @@ components: type: string format: uuid description: "The user Id who updated the work period payment last time.(Will get the user info from the token)" + WorkPeriodPaymentCreatedError: + required: + - workPeriodId + properties: + workPeriodId: + type: string + format: uuid + description: "The work period id." + amount: + type: integer + example: 2 + description: "The amount to be paid." + error: + type: object + properties: + message: + type: string + description: "The error message" + code: + type: integer + example: 429 + description: "HTTP code of error" WorkPeriodPaymentRequestBody: required: - workPeriodId @@ -4247,8 +4332,85 @@ components: description: "The amount to be paid." status: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "cancelled"] description: "The payment status." + WorkPeriodPaymentCreateRequestBody: + required: + - workPeriodId + properties: + workPeriodId: + type: string + format: uuid + description: "The work period id." + amount: + type: integer + example: 2 + description: "The amount to be paid." + WorkPeriodPaymentQueryCreateRequestBody: + properties: + status: + type: string + enum: ["placed", "in-progress", "completed"] + description: The resource booking status. + startDate: + type: string + format: date + description: The resource booking start date. + endDate: + type: string + format: date + description: The resource booking end date. + rateType: + type: string + enum: ["hourly", "daily", "weekly", "monthly"] + description: The resource booking rate type. + jobId: + type: string + format: uuid + description: The job id. + userId: + type: string + format: uuid + description: The user id. + projectId: + type: integer + description: The project id. + projectIds: + oneOf: + - type: string + description: comma separated project ids. + - type: array + items: + type: integer + workPeriods.paymentStatus: + type: string + enum: ["pending", "partially-completed", "completed", "cancelled"] + workPeriods.startDate: + type: string + format: date + pattern: '^\d{4}-\d{2}-\d{2}$' + description: The work period start date. + workPeriods.endDate: + type: string + format: date + pattern: '^\d{4}-\d{2}-\d{2}$' + description: The work period end date. + workPeriods.userHandle: + type: string + description: The user handle. + WorkPeriodPaymentQueryCreateResult: + properties: + total: + type: integer + description: The total Work Periods found. + totalSuccess: + type: integer + description: The total payments scheduled successfully. + totalError: + type: integer + description: The total payments which failed to get scheduled. + query: + $ref: "#/components/schemas/WorkPeriodPaymentQueryCreateRequestBody" WorkPeriodPaymentPatchRequestBody: properties: workPeriodId: @@ -4261,7 +4423,7 @@ components: description: "The amount to be paid." status: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "cancelled"] description: "The payment status." CheckRun: type: object diff --git a/migrations/2021-05-26-work-period-payment-table-migration.js b/migrations/2021-05-26-work-period-payment-table-migration.js new file mode 100644 index 00000000..5ee8ef70 --- /dev/null +++ b/migrations/2021-05-26-work-period-payment-table-migration.js @@ -0,0 +1,38 @@ +'use strict'; +const config = require('config') + +/** + * Migrate work_period_payments challenge_id - from not null to allow null. + * enum_work_period_payments_status from completed, cancelled to completed, canceled, scheduled. + */ +module.exports = { + up: async (queryInterface, Sequelize) => { + const table = { tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME } + await Promise.all([ + queryInterface.changeColumn(table, 'challenge_id', { type: Sequelize.UUID }), + queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'scheduled'`) + ]) + }, + + down: async (queryInterface, Sequelize) => { + const table = { tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME } + await Promise.all([ + queryInterface.changeColumn(table, 'challenge_id', { type: Sequelize.UUID, allowNull: false }), + queryInterface.sequelize.query(` + DELETE + FROM + pg_enum + WHERE + enumlabel = 'scheduled' AND + enumtypid = ( + SELECT + oid + FROM + pg_type + WHERE + typname = 'enum_work_period_payments_status' + ) + `) + ]) + } +}; diff --git a/src/bootstrap.js b/src/bootstrap.js index 2999f131..9ca05431 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -2,10 +2,8 @@ const fs = require('fs') const Joi = require('joi') const path = require('path') const _ = require('lodash') -const { Interviews } = require('../app-constants') +const { Interviews, WorkPeriodPaymentStatus } = require('../app-constants') const logger = require('./common/logger') -const constants = require('../app-constants') -const config = require('config') const allowedInterviewStatuses = _.values(Interviews.Status) const allowedXAITemplate = _.keys(Interviews.XaiTemplate) @@ -16,12 +14,12 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') -Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed') +Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed') Joi.title = () => Joi.string().max(128) Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) Joi.interviewStatus = () => Joi.string().valid(...allowedInterviewStatuses) -Joi.workPeriodPaymentStatus = () => Joi.string().valid('completed', 'cancelled') +Joi.workPeriodPaymentStatus = () => Joi.string().valid(..._.values(WorkPeriodPaymentStatus)) // Empty string is not allowed by Joi by default and must be enabled with allow(''). // See https://joi.dev/api/?v=17.3.0#string fro details why it's like this. // In many cases we would like to allow empty string to make it easier to create UI for editing data. @@ -45,14 +43,3 @@ function buildServices (dir) { } buildServices(path.join(__dirname, 'services')) - -// validate some configurable parameters for the app -const paymentProcessingSwitchSchema = Joi.string().label('PAYMENT_PROCESSING_SWITCH').valid( - ...Object.values(constants.PaymentProcessingSwitch) -) -try { - Joi.attempt(config.PAYMENT_PROCESSING_SWITCH, paymentProcessingSwitchSchema) -} catch (err) { - console.error(err.message) - process.exit(1) -} diff --git a/src/controllers/WorkPeriodPaymentController.js b/src/controllers/WorkPeriodPaymentController.js index 93f5c046..4bba2385 100644 --- a/src/controllers/WorkPeriodPaymentController.js +++ b/src/controllers/WorkPeriodPaymentController.js @@ -3,7 +3,6 @@ */ const service = require('../services/WorkPeriodPaymentService') const helper = require('../common/helper') -const config = require('config') /** * Get workPeriodPayment by id @@ -20,7 +19,7 @@ async function getWorkPeriodPayment (req, res) { * @param res the response */ async function createWorkPeriodPayment (req, res) { - res.send(await service.createWorkPeriodPayment(req.authUser, req.body, { paymentProcessingSwitch: config.PAYMENT_PROCESSING_SWITCH })) + res.send(await service.createWorkPeriodPayment(req.authUser, req.body)) } /** @@ -52,9 +51,19 @@ async function searchWorkPeriodPayments (req, res) { res.send(result.result) } +/** + * Create all query workPeriodPayments + * @param req the request + * @param res the response + */ +async function createQueryWorkPeriodPayments (req, res) { + res.send(await service.createQueryWorkPeriodPayments(req.authUser, req.body)) +} + module.exports = { getWorkPeriodPayment, createWorkPeriodPayment, + createQueryWorkPeriodPayments, partiallyUpdateWorkPeriodPayment, fullyUpdateWorkPeriodPayment, searchWorkPeriodPayments diff --git a/src/models/WorkPeriodPayment.js b/src/models/WorkPeriodPayment.js index 3683faf0..7db484ea 100644 --- a/src/models/WorkPeriodPayment.js +++ b/src/models/WorkPeriodPayment.js @@ -1,6 +1,8 @@ const { Sequelize, Model } = require('sequelize') +const _ = require('lodash') const config = require('config') const errors = require('../common/errors') +const { WorkPeriodPaymentStatus } = require('../../app-constants') module.exports = (sequelize) => { class WorkPeriodPayment extends Model { @@ -44,17 +46,13 @@ module.exports = (sequelize) => { }, challengeId: { field: 'challenge_id', - type: Sequelize.UUID, - allowNull: false + type: Sequelize.UUID }, amount: { type: Sequelize.DOUBLE }, status: { - type: Sequelize.ENUM( - 'completed', - 'cancelled' - ), + type: Sequelize.ENUM(_.values(WorkPeriodPaymentStatus)), allowNull: false }, billingAccountId: { diff --git a/src/routes/WorkPeriodPaymentRoutes.js b/src/routes/WorkPeriodPaymentRoutes.js index dcc284eb..3b6f6ba9 100644 --- a/src/routes/WorkPeriodPaymentRoutes.js +++ b/src/routes/WorkPeriodPaymentRoutes.js @@ -18,6 +18,14 @@ module.exports = { scopes: [constants.Scopes.READ_WORK_PERIOD_PAYMENT, constants.Scopes.ALL_WORK_PERIOD_PAYMENT] } }, + '/work-period-payments/query': { + post: { + controller: 'WorkPeriodPaymentController', + method: 'createQueryWorkPeriodPayments', + auth: 'jwt', + scopes: [constants.Scopes.CREATE_WORK_PERIOD_PAYMENT, constants.Scopes.ALL_WORK_PERIOD_PAYMENT] + } + }, '/work-period-payments/:id': { get: { controller: 'WorkPeriodPaymentController', diff --git a/src/services/InterviewService.js b/src/services/InterviewService.js index 10a065f4..a69a788c 100644 --- a/src/services/InterviewService.js +++ b/src/services/InterviewService.js @@ -241,8 +241,8 @@ async function requestInterview (currentUser, jobCandidateId, interview) { 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] + var foundGuestMember = _.find(guestMembers, function (guestMember) { return guestEmail === guestMember.email }) + return (foundGuestMember !== undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split('@')[0] }) try { diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index f5c40206..f1758c89 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -472,7 +472,6 @@ async function searchResourceBookings (currentUser, criteria, options = { return criteria.sortOrder = 'desc' } try { - throw new Error('fallback to DB') const esQuery = { index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), _source_includes: queryOpt.include, diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index c196f88e..59f63720 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -3,18 +3,17 @@ */ const _ = require('lodash') -const Joi = require('joi') +const Joi = require('joi').extend(require('@joi/date')) const config = require('config') const HttpStatus = require('http-status-codes') const { Op } = require('sequelize') const uuid = require('uuid') -const moment = require('moment') const helper = require('../common/helper') const logger = require('../common/logger') const errors = require('../common/errors') -const constants = require('../../app-constants') const models = require('../models') -const PaymentService = require('./PaymentService') +const { WorkPeriodPaymentStatus } = require('../../app-constants') +const { searchResourceBookings } = require('./ResourceBookingService') const WorkPeriodPayment = models.WorkPeriodPayment const esClient = helper.getESClient() @@ -32,6 +31,75 @@ async function _checkUserPermissionForCRUWorkPeriodPayment (currentUser) { } } +/** + * Create single workPeriodPayment + * @param {Object} workPeriodPayment the workPeriodPayment to be created + * @param {String} createdBy the authUser id + * @returns {Object} the created workPeriodPayment + */ +async function _createSingleWorkPeriodPayment (workPeriodPayment, createdBy) { + const correspondingWorkPeriod = await helper.ensureWorkPeriodById(workPeriodPayment.workPeriodId) // ensure work period exists + + // get billingAccountId from corresponding resource booking + const correspondingResourceBooking = await helper.ensureResourceBookingById(correspondingWorkPeriod.resourceBookingId) + + return _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking(workPeriodPayment, createdBy, correspondingWorkPeriod, correspondingResourceBooking) +} + +/** + * Create single workPeriodPayment + * @param {Object} workPeriodPayment the workPeriodPayment to be created + * @param {String} createdBy the authUser id + * @param {Object} correspondingWorkPeriod the workPeriod + * @param {Object} correspondingResourceBooking the resourceBooking + * @returns {Object} the created workPeriodPayment + */ +async function _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking (workPeriodPayment, createdBy, correspondingWorkPeriod, correspondingResourceBooking) { + if (!correspondingResourceBooking.billingAccountId) { + throw new errors.ConflictError(`id: ${correspondingWorkPeriod.resourceBookingId} "ResourceBooking" Billing account is not assigned to the resource booking`) + } + workPeriodPayment.billingAccountId = correspondingResourceBooking.billingAccountId + workPeriodPayment.id = uuid.v4() + workPeriodPayment.status = WorkPeriodPaymentStatus.SCHEDULED + workPeriodPayment.createdBy = createdBy + + // set workPeriodPayment amount + if (_.isNil(workPeriodPayment.amount)) { + const memberRate = correspondingWorkPeriod.memberRate || correspondingResourceBooking.memberRate + if (_.isNil(memberRate)) { + throw new errors.BadRequestError(`Can't find a member rate in work period: ${workPeriodPayment.workPeriodId} to calculate the amount`) + } + let daysWorked = 0 + if (correspondingWorkPeriod.daysWorked) { + daysWorked = correspondingWorkPeriod.daysWorked + } else { + const matchDW = _.find(helper.extractWorkPeriods(correspondingResourceBooking.startDate, correspondingResourceBooking.endDate), { startDate: correspondingWorkPeriod.startDate }) + if (matchDW) { + daysWorked = matchDW.daysWorked + } + } + if (daysWorked === 0) { + workPeriodPayment.amount = 0 + } else { + workPeriodPayment.amount = _.round(memberRate * 5 / daysWorked, 2) + } + } + + let created = null + try { + created = await WorkPeriodPayment.create(workPeriodPayment) + } catch (err) { + if (!_.isUndefined(err.original)) { + throw new errors.BadRequestError(err.original.detail) + } else { + throw err + } + } + + await helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC, created.toJSON()) + return created.dataValues +} + /** * Get workPeriodPayment by id * @param {Object} currentUser the user who perform this operation. @@ -101,58 +169,39 @@ getWorkPeriodPayment.schema = Joi.object().keys({ * Create workPeriodPayment * @param {Object} currentUser the user who perform this operation * @param {Object} workPeriodPayment the workPeriodPayment to be created - * @param {Object} options the extra options to control the function * @returns {Object} the created workPeriodPayment */ -async function createWorkPeriodPayment (currentUser, workPeriodPayment, options = { paymentProcessingSwitch: 'OFF' }) { +async function createWorkPeriodPayment (currentUser, workPeriodPayment) { // check permission await _checkUserPermissionForCRUWorkPeriodPayment(currentUser) + const createdBy = await helper.getUserId(currentUser.userId) - const { projectId, userHandle, endDate, resourceBookingId } = await helper.ensureWorkPeriodById(workPeriodPayment.workPeriodId) // ensure work period exists - - // get billingAccountId from corresponding resource booking - const correspondingResourceBooking = await helper.ensureResourceBookingById(resourceBookingId) - if (!correspondingResourceBooking.billingAccountId) { - throw new errors.ConflictError(`id: ${resourceBookingId} "ResourceBooking" Billing account is not assigned to the resource booking`) - } - workPeriodPayment.billingAccountId = correspondingResourceBooking.billingAccountId - - const paymentChallenge = options.paymentProcessingSwitch === constants.PaymentProcessingSwitch.ON ? (await PaymentService.createPayment({ - projectId, - userHandle, - amount: workPeriodPayment.amount, - name: `TaaS Payment - ${userHandle} - Week Ending ${moment(endDate).format('D/M/YYYY')}`, - description: `TaaS Payment - ${userHandle} - Week Ending ${moment(endDate).format('D/M/YYYY')}`, - billingAccountId: correspondingResourceBooking.billingAccountId - })) : ({ id: '00000000-0000-0000-0000-000000000000' }) - - workPeriodPayment.id = uuid.v4() - workPeriodPayment.challengeId = paymentChallenge.id - workPeriodPayment.createdBy = await helper.getUserId(currentUser.userId) - - let created = null - try { - created = await WorkPeriodPayment.create(workPeriodPayment) - } catch (err) { - if (!_.isUndefined(err.original)) { - throw new errors.BadRequestError(err.original.detail) - } else { - throw err + if (_.isArray(workPeriodPayment)) { + const result = [] + for (const wp of workPeriodPayment) { + try { + const successResult = await _createSingleWorkPeriodPayment(wp, createdBy) + result.push(successResult) + } catch (e) { + result.push(_.extend(wp, { error: { message: e.message, code: e.httpStatus } })) + } } + return result + } else { + return await _createSingleWorkPeriodPayment(workPeriodPayment, createdBy) } - - await helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC, created.toJSON()) - return created.dataValues } +const singleCreateWorkPeriodPaymentSchema = Joi.object().keys({ + workPeriodId: Joi.string().uuid().required(), + amount: Joi.number().greater(0).allow(null) +}) createWorkPeriodPayment.schema = Joi.object().keys({ currentUser: Joi.object().required(), - workPeriodPayment: Joi.object().keys({ - workPeriodId: Joi.string().uuid().required(), - amount: Joi.number().greater(0).allow(null), - status: Joi.workPeriodPaymentStatus().default('completed') - }).required(), - options: Joi.object() + workPeriodPayment: Joi.alternatives().try( + singleCreateWorkPeriodPaymentSchema.required(), + Joi.array().min(1).items(singleCreateWorkPeriodPaymentSchema).required() + ).required() }).required() /** @@ -358,9 +407,67 @@ searchWorkPeriodPayments.schema = Joi.object().keys({ options: Joi.object() }).required() +/** + * Create all query workPeriodPayments + * @param {Object} currentUser the user who perform this operation. + * @param {Object} criteria the query criteria + * @returns {Object} the process result + */ +async function createQueryWorkPeriodPayments (currentUser, criteria) { + // check permission + await _checkUserPermissionForCRUWorkPeriodPayment(currentUser) + const createdBy = await helper.getUserId(currentUser.userId) + const query = criteria.query + + const fields = _.join(_.uniq(_.concat( + ['id', 'billingAccountId', 'memberRate', 'startDate', 'endDate', 'workPeriods.id', 'workPeriods.resourceBookingId', 'workPeriods.memberRate', 'workPeriods.daysWorked', 'workPeriods.startDate'], + _.map(_.keys(query), k => k === 'projectIds' ? 'projectId' : k)) + ), ',') + const searchResult = await searchResourceBookings(currentUser, _.extend({ fields, page: 1 }, query), { returnAll: true }) + + const wpArray = _.flatMap(searchResult.result, 'workPeriods') + const resourceBookingMap = _.fromPairs(_.map(searchResult.result, rb => [rb.id, rb])) + const result = { total: wpArray.length, query, totalSuccess: 0, totalError: 0 } + + for (const wp of wpArray) { + try { + await _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking({ workPeriodId: wp.id }, createdBy, wp, resourceBookingMap[wp.resourceBookingId]) + result.totalSuccess++ + } catch (err) { + logger.logFullError(err, { component: 'WorkPeriodPaymentService', context: 'createQueryWorkPeriodPayments' }) + result.totalError++ + } + } + return result +} + +createQueryWorkPeriodPayments.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + criteria: Joi.object().keys({ + query: Joi.object().keys({ + status: Joi.resourceBookingStatus(), + startDate: Joi.date().format('YYYY-MM-DD'), + endDate: Joi.date().format('YYYY-MM-DD'), + rateType: Joi.rateType(), + jobId: Joi.string().uuid(), + userId: Joi.string().uuid(), + projectId: Joi.number().integer(), + projectIds: Joi.alternatives( + Joi.string(), + Joi.array().items(Joi.number().integer()) + ), + 'workPeriods.paymentStatus': Joi.paymentStatus(), + 'workPeriods.startDate': Joi.date().format('YYYY-MM-DD'), + 'workPeriods.endDate': Joi.date().format('YYYY-MM-DD'), + 'workPeriods.userHandle': Joi.string() + }).required() + }).required() +}).required() + module.exports = { getWorkPeriodPayment, createWorkPeriodPayment, + createQueryWorkPeriodPayments, partiallyUpdateWorkPeriodPayment, fullyUpdateWorkPeriodPayment, searchWorkPeriodPayments diff --git a/test/unit/ResourceBookingService.test.js b/test/unit/ResourceBookingService.test.js index 64de1900..862ef357 100644 --- a/test/unit/ResourceBookingService.test.js +++ b/test/unit/ResourceBookingService.test.js @@ -455,9 +455,13 @@ describe('resourceBooking service test', () => { const stubResourceBookingFindAll = sinon.stub(ResourceBooking, 'findAll').callsFake(async () => { return data.resourceBookingFindAll }) + const stubResourceBookingCount = sinon.stub(ResourceBooking, 'count').callsFake(async () => { + return data.resourceBookingFindAll.length + }) const result = await service.searchResourceBookings(commonData.userWithManagePermission, data.criteria) expect(esClientSearch.calledOnce).to.be.true expect(stubResourceBookingFindAll.calledOnce).to.be.true + expect(stubResourceBookingCount.calledOnce).to.be.true expect(result).to.deep.eq(data.result) }) it('T26:Fail to search resource booking with not allowed fields', async () => { diff --git a/test/unit/WorkPeriodPaymentService.test.js b/test/unit/WorkPeriodPaymentService.test.js index 5e90b072..ecc11186 100644 --- a/test/unit/WorkPeriodPaymentService.test.js +++ b/test/unit/WorkPeriodPaymentService.test.js @@ -5,7 +5,6 @@ const expect = require('chai').expect const sinon = require('sinon') const models = require('../../src/models') const service = require('../../src/services/WorkPeriodPaymentService') -const paymentService = require('../../src/services/PaymentService') const commonData = require('./common/CommonData') const testData = require('./common/WorkPeriodPaymentData') const helper = require('../../src/common/helper') @@ -25,32 +24,25 @@ describe('workPeriod service test', () => { let stubEnsureWorkPeriodById let stubEnsureResourceBookingById let stubCreateWorkPeriodPayment - let stubCreatePayment beforeEach(async () => { stubGetUserId = sinon.stub(helper, 'getUserId').callsFake(async () => testData.workPeriodPayment01.getUserIdResponse) stubEnsureWorkPeriodById = sinon.stub(helper, 'ensureWorkPeriodById').callsFake(async () => testData.workPeriodPayment01.ensureWorkPeriodByIdResponse) stubEnsureResourceBookingById = sinon.stub(helper, 'ensureResourceBookingById').callsFake(async () => testData.workPeriodPayment01.ensureResourceBookingByIdResponse) stubCreateWorkPeriodPayment = sinon.stub(models.WorkPeriodPayment, 'create').callsFake(() => testData.workPeriodPayment01.response) - stubCreatePayment = sinon.stub(paymentService, 'createPayment').callsFake(async () => testData.workPeriodPayment01.createPaymentResponse) }) it('create work period success', async () => { - const response = await service.createWorkPeriodPayment(commonData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'ON' }) + const response = await service.createWorkPeriodPayment(commonData.currentUser, testData.workPeriodPayment01.request) expect(stubGetUserId.calledOnce).to.be.true expect(stubEnsureWorkPeriodById.calledOnce).to.be.true expect(stubEnsureResourceBookingById.calledOnce).to.be.true - expect(stubCreatePayment.calledOnce).to.be.true expect(stubCreateWorkPeriodPayment.calledOnce).to.be.true expect(response).to.eql(testData.workPeriodPayment01.response.dataValues) }) it('create work period success - billingAccountId is set', async () => { - await service.createWorkPeriodPayment(commonData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'ON' }) - expect(stubCreatePayment.calledOnce).to.be.true - expect(stubCreatePayment.args[0][0]).to.include({ - billingAccountId: testData.workPeriodPayment01.ensureResourceBookingByIdResponse.billingAccountId - }) + await service.createWorkPeriodPayment(commonData.currentUser, testData.workPeriodPayment01.request) expect(stubCreateWorkPeriodPayment.calledOnce).to.be.true expect(stubCreateWorkPeriodPayment.args[0][0]).to.include({ billingAccountId: testData.workPeriodPayment01.ensureResourceBookingByIdResponse.billingAccountId @@ -67,16 +59,5 @@ describe('workPeriod service test', () => { expect(err.message).to.include('"ResourceBooking" Billing account is not assigned to the resource booking') } }) - - describe('when PAYMENT_PROCESSING_SWITCH is ON/OFF', async () => { - it('do not create payment if PAYMENT_PROCESSING_SWITCH is OFF', async () => { - await service.createWorkPeriodPayment(commonData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'OFF' }) - expect(stubCreatePayment.calledOnce).to.be.false - }) - it('create payment if PAYMENT_PROCESSING_SWITCH is ON', async () => { - await service.createWorkPeriodPayment(commonData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'ON' }) - expect(stubCreatePayment.calledOnce).to.be.true - }) - }) }) }) diff --git a/test/unit/common/WorkPeriodPaymentData.js b/test/unit/common/WorkPeriodPaymentData.js index d94d9280..6e321b62 100644 --- a/test/unit/common/WorkPeriodPaymentData.js +++ b/test/unit/common/WorkPeriodPaymentData.js @@ -1,14 +1,13 @@ const workPeriodPayment01 = { request: { workPeriodId: '467b4df7-ced4-41b9-9710-b83808cddaf4', - amount: 600, - status: 'completed' + amount: 600 }, response: { dataValues: { workPeriodId: '467b4df7-ced4-41b9-9710-b83808cddaf4', amount: 600, - status: 'completed', + status: 'scheduled', id: '01971e6f-0f09-4a2a-bc2e-2adac0f00622', challengeId: '00000000-0000-0000-0000-000000000000', createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', From 67c82a7b909dfdd4178f847e8f9352d3421b22dc Mon Sep 17 00:00:00 2001 From: xxcxy Date: Wed, 2 Jun 2021 20:46:22 +0800 Subject: [PATCH 30/86] Batch Payments - Part 1 - Scheduler --- README.md | 1 + app-constants.js | 9 + app.js | 4 + config/default.js | 35 +- data/demo-data.json | 36 +- docs/swagger.yaml | 22 +- ...ler-table-add-status-details-to-payment.js | 109 ++++++ package.json | 1 + scripts/demo-payment-scheduler/data.json | 103 ++++++ scripts/demo-payment-scheduler/index.js | 81 +++++ src/bootstrap.js | 6 +- src/common/helper.js | 72 ++++ .../WorkPeriodPaymentController.js | 2 +- src/models/PaymentScheduler.js | 107 ++++++ src/models/WorkPeriodPayment.js | 14 +- src/services/InterviewService.js | 4 +- src/services/PaymentSchedulerService.js | 334 ++++++++++++++++++ src/services/PaymentService.js | 34 +- src/services/ResourceBookingService.js | 1 - test/unit/ResourceBookingService.test.js | 3 + 20 files changed, 938 insertions(+), 40 deletions(-) create mode 100644 migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js create mode 100644 scripts/demo-payment-scheduler/data.json create mode 100644 scripts/demo-payment-scheduler/index.js create mode 100644 src/models/PaymentScheduler.js create mode 100644 src/services/PaymentSchedulerService.js diff --git a/README.md b/README.md index 5e3895c2..6d41ba20 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,7 @@ To be able to change and test `taas-es-processor` locally you can follow the nex | `npm run cov` | Code Coverage Report. | | `npm run migrate` | Run any migration files which haven't run yet. | | `npm run migrate:undo` | Revert most recent migration. | +| `npm run demo-payment-scheduler` | Create 1000 Work Periods Payment records in with status "scheduled" and various "amount" | ## Import and Export data diff --git a/app-constants.js b/app-constants.js index 534e46de..2b45f516 100644 --- a/app-constants.js +++ b/app-constants.js @@ -76,6 +76,14 @@ const ChallengeStatus = { COMPLETED: 'Completed' } +const WorkPeriodPaymentStatus = { + COMPLETED: 'completed', + SCHEDULED: 'scheduled', + IN_PROGRESS: 'in-progress', + FAILED: 'failed', + CANCELLED: 'cancelled' +} + const PaymentProcessingSwitch = { ON: 'ON', OFF: 'OFF' @@ -87,5 +95,6 @@ module.exports = { Scopes, Interviews, ChallengeStatus, + WorkPeriodPaymentStatus, PaymentProcessingSwitch } diff --git a/app.js b/app.js index 7f3d7d85..e6d79c69 100644 --- a/app.js +++ b/app.js @@ -13,6 +13,7 @@ const schedule = require('node-schedule') const logger = require('./src/common/logger') const eventHandlers = require('./src/eventHandlers') const interviewService = require('./src/services/InterviewService') +const { processScheduler } = require('./src/services/PaymentSchedulerService') // setup express app const app = express() @@ -97,6 +98,9 @@ const server = app.listen(app.get('port'), () => { eventHandlers.init() // schedule updateCompletedInterviews to run every hour schedule.scheduleJob('0 0 * * * *', interviewService.updateCompletedInterviews) + + // schedule payment processing + schedule.scheduleJob(config.PAYMENT_PROCESSING.CRON, processScheduler) }) if (process.env.NODE_ENV === 'test') { diff --git a/config/default.js b/config/default.js index 2b5ca7ba..59501079 100644 --- a/config/default.js +++ b/config/default.js @@ -162,5 +162,38 @@ 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 off actual API calls in Payment Scheduler + SWITCH: process.env.PAYMENT_PROCESSING_SWITCH || 'OFF', + // the payment scheduler cron config + CRON: process.env.PAYMENT_PROCESSING_CRON || '0 */5 * * * *', + // the number of records processed by one time + BATCH_SIZE: parseInt(process.env.PAYMENT_PROCESSING_BATCH_SIZE || 50), + // in-progress expired to determine whether a record has been processed abnormally, moment duration format + IN_PROGRESS_EXPIRED: process.env.IN_PROGRESS_EXPIRED || 'PT1H', + // the number of max retry config + MAX_RETRY_COUNT: parseInt(process.env.PAYMENT_PROCESSING_MAX_RETRY_COUNT || 10), + // the time of retry base delay, unit: ms + RETRY_BASE_DELAY: parseInt(process.env.PAYMENT_PROCESSING_RETRY_BASE_DELAY || 100), + // the time of retry max delay, unit: ms + RETRY_MAX_DELAY: parseInt(process.env.PAYMENT_PROCESSING_RETRY_MAX_DELAY || 10000), + // the max time of one request, unit: ms + PER_REQUEST_MAX_TIME: parseInt(process.env.PAYMENT_PROCESSING_PER_REQUEST_MAX_TIME || 30000), + // the max time of one payment record, unit: ms + PER_PAYMENT_MAX_TIME: parseInt(process.env.PAYMENT_PROCESSING_PER_PAYMENT_MAX_TIME || 60000), + // the max records of payment of a minute + PER_MINUTE_PAYMENT_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_PAYMENT_MAX_COUNT || 12), + // the max requests of challenge of a minute + PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT || 60), + // the max requests of resource of a minute + PER_MINUTE_RESOURCE_REQUEST_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT || 20), + // the default step fix delay, unit: ms + FIX_DELAY_STEP: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500), + // the fix delay between step one and step two, unit: ms + FIX_DELAY_STEP_1_2: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_1_2 || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500), + // the fix delay between step two and step three, unit: ms + FIX_DELAY_STEP_2_3: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_2_3 || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500), + // the fix delay between step three and step four, unit: ms + FIX_DELAY_STEP_3_4: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_3_4 || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500) + } } diff --git a/data/demo-data.json b/data/demo-data.json index e0733443..e7fbe36e 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -182,12 +182,12 @@ { "id": "077aa2ca-5b60-4ad9-a965-1b37e08a5046", "jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5", - "googleCalendarId": null, + "calendarEventId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Completed", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, @@ -211,12 +211,12 @@ { "id": "b1f7ba76-640f-47e2-9463-59e51b51ec60", "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": "dummyId", + "calendarEventId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Scheduling", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, @@ -226,12 +226,12 @@ { "id": "3144fa65-ea1a-4bec-81b0-7cb1c8845826", "jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff", - "googleCalendarId": null, + "calendarEventId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Completed", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, @@ -255,12 +255,12 @@ { "id": "976d23a9-5710-453f-99d9-f57a588bb610", "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", + "calendarEventId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 3, "startTimestamp": null, - "attendeesList": [ + "guestEmails": [ "attendee1@yopmail.com", "attendee2@yopmail.com" ], @@ -273,12 +273,12 @@ { "id": "a23e1bf2-1084-4cfe-a0d8-d83bc6fec655", "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": "dummyId", + "calendarEventId": "dummyId", "customMessage": "This is a custom message", - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 2, "startTimestamp": null, - "attendeesList": [ + "guestEmails": [ "attendee1@yopmail.com", "attendee2@yopmail.com" ], @@ -291,12 +291,12 @@ { "id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb", "jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36", - "googleCalendarId": null, + "calendarEventId": null, "customMessage": null, - "xaiTemplate": "interview-30", + "templateUrl": "interview-30", "round": 1, "startTimestamp": null, - "attendeesList": null, + "guestEmails": null, "status": "Completed", "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a0b6064b..cf34e0a4 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2380,7 +2380,7 @@ paths: required: false schema: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"] description: The payment status. responses: "200": @@ -4211,8 +4211,22 @@ components: description: "The amount to be paid." status: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"] description: "The payment status." + statusDetails: + type: object + properties: + errorMessage: + type: string + errorCode: + type: integer + retry: + type: integer + step: + type: string + challengeId: + type: string + format: uuid billingAccountId: type: integer example: 80000071 @@ -4247,7 +4261,7 @@ components: description: "The amount to be paid." status: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"] description: "The payment status." WorkPeriodPaymentPatchRequestBody: properties: @@ -4261,7 +4275,7 @@ components: description: "The amount to be paid." status: type: string - enum: ["completed", "cancelled"] + enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"] description: "The payment status." CheckRun: type: object diff --git a/migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js b/migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js new file mode 100644 index 00000000..40c1596b --- /dev/null +++ b/migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js @@ -0,0 +1,109 @@ +'use strict'; + +const config = require('config') +const _ = require('lodash') + +/** + * Create `payment_schedulers` table & relations. + */ +module.exports = { + up: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.createTable('payment_schedulers', { + id: { + type: Sequelize.UUID, + primaryKey: true, + allowNull: false, + defaultValue: Sequelize.UUIDV4 + }, + challengeId: { + field: 'challenge_id', + type: Sequelize.UUID, + allowNull: false + }, + workPeriodPaymentId: { + field: 'work_period_payment_id', + type: Sequelize.UUID, + allowNull: false, + references: { + model: { + tableName: 'work_period_payments', + schema: config.DB_SCHEMA_NAME + }, + key: 'id' + } + }, + step: { + type: Sequelize.INTEGER, + allowNull: false + }, + status: { + type: Sequelize.ENUM( + 'in-progress', + 'completed', + 'failed' + ), + allowNull: false + }, + userId: { + field: 'user_id', + type: Sequelize.BIGINT + }, + userHandle: { + field: 'user_handle', + type: Sequelize.STRING, + allowNull: false + }, + 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, transaction }) + await queryInterface.addColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'status_details', + { type: Sequelize.JSONB }, + { transaction }) + await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'challenge_id', + { type: Sequelize.UUID }, + { transaction }) + await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'scheduled'`) + await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'in-progress'`) + await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'failed'`) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + }, + + down: async (queryInterface, Sequelize) => { + const table = { schema: config.DB_SCHEMA_NAME, tableName: 'payment_schedulers' } + const statusTypeName = `${table.schema}.enum_${table.tableName}_status` + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.dropTable(table, { transaction }) + // drop enum type for status column + await queryInterface.sequelize.query(`DROP TYPE ${statusTypeName}`, { transaction }) + + await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'challenge_id', + { type: Sequelize.UUID, allowNull: false }, + { transaction }) + await queryInterface.removeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'status_details', + { transaction }) + await queryInterface.sequelize.query(`DELETE FROM pg_enum WHERE enumlabel in ('scheduled', 'in-progress', 'failed') AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'enum_work_period_payments_status')`, + { transaction }) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + } +}; diff --git a/package.json b/package.json index 0fa24cca..f434c7b8 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "local:init": "npm run local:reset && npm run data:import -- --force", "local:reset": "npm run delete-index -- --force || true && npm run create-index -- --force && npm run init-db force", "cov": "nyc --reporter=html --reporter=text npm run test", + "demo-payment-scheduler": "node scripts/demo-payment-scheduler/index.js && npm run index:all -- --force", "demo-payment": "node scripts/demo-payment" }, "keywords": [], diff --git a/scripts/demo-payment-scheduler/data.json b/scripts/demo-payment-scheduler/data.json new file mode 100644 index 00000000..5023842c --- /dev/null +++ b/scripts/demo-payment-scheduler/data.json @@ -0,0 +1,103 @@ +{ + "Job":{ + "id":"43d695d4-e926-41d5-ad42-a899612b5246", + "projectId":17234, + "title":"Dummy title - at most 64 characters", + "numPositions":13, + "skills":[ + "23e00d92-207a-4b5b-b3c9-4c5662644941", + "7d076384-ccf6-4e43-a45d-1b24b1e624aa", + "cbac57a3-7180-4316-8769-73af64893158", + "a2b4bc11-c641-4a19-9eb7-33980378f82e" + ], + "status":"in-review", + "isApplicationPageActive":false, + "createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy":"00000000-0000-0000-0000-000000000000", + "createdAt":"2021-05-09T21:21:10.394Z", + "updatedAt":"2021-05-09T21:21:14.010Z" + }, + "ResourceBooking":{ + "id":"41671764-0ded-46fd-b7de-2af5d5e4f3fc", + "projectId":17234, + "userId":"05e988b7-7d54-4c10-ada1-1a04870a88a8", + "jobId":"43d695d4-e926-41d5-ad42-a899612b5246", + "status":"placed", + "startDate":"2020-09-27", + "endDate":"2020-10-27", + "memberRate":13.23, + "customerRate":13, + "rateType":"hourly", + "billingAccountId":80000069, + "createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy":null, + "createdAt":"2021-05-09T21:25:46.728Z", + "updatedAt":"2021-05-09T21:25:46.728Z" + }, + "WorkPeriods":[ + { + "id":"4baae2cf-fd70-4ab3-9959-e826257b7e0f", + "resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc", + "userHandle":"pshah_manager", + "projectId":17234, + "startDate":"2020-09-27", + "endDate":"2020-10-03", + "daysWorked":4, + "memberRate":27.06, + "customerRate":13.13, + "paymentStatus":"partially-completed", + "createdBy":"00000000-0000-0000-0000-000000000000", + "updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt":"2021-05-09T21:25:47.813Z", + "updatedAt":"2021-05-09T21:45:32.659Z" + }, + { + "id":"9918e1b7-acbc-41ae-baa6-fdcb2386681d", + "resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc", + "userHandle":"Shuchikr", + "projectId":17234, + "startDate":"2020-10-18", + "endDate":"2020-10-24", + "daysWorked":4, + "memberRate":4.08, + "customerRate":3.89, + "paymentStatus":"cancelled", + "createdBy":"00000000-0000-0000-0000-000000000000", + "updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt":"2021-05-09T21:25:47.834Z", + "updatedAt":"2021-05-09T21:45:37.647Z" + }, + { + "id":"42e990c9-b14c-4496-9977-c3024aa90024", + "resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc", + "userHandle":"vkumars", + "projectId":17234, + "startDate":"2020-10-25", + "endDate":"2020-10-31", + "daysWorked":3, + "memberRate":15.61, + "customerRate":9.76, + "paymentStatus":"pending", + "createdBy":"00000000-0000-0000-0000-000000000000", + "updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt":"2021-05-09T21:25:47.824Z", + "updatedAt":"2021-05-09T21:45:48.727Z" + }, + { + "id":"8bf64481-ae7b-4e51-b48c-000cd90c87d1", + "resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc", + "userHandle":"chandanant", + "projectId":17234, + "startDate":"2020-10-11", + "endDate":"2020-10-17", + "daysWorked":4, + "memberRate":10.82, + "customerRate":30.71, + "paymentStatus":"pending", + "createdBy":"00000000-0000-0000-0000-000000000000", + "updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt":"2021-05-09T21:25:47.815Z", + "updatedAt":"2021-05-09T21:45:41.810Z" + } + ] + } \ No newline at end of file diff --git a/scripts/demo-payment-scheduler/index.js b/scripts/demo-payment-scheduler/index.js new file mode 100644 index 00000000..8f76a814 --- /dev/null +++ b/scripts/demo-payment-scheduler/index.js @@ -0,0 +1,81 @@ +const { v4: uuid } = require('uuid') +const config = require('config') +const _ = require('lodash') +const data = require('./data.json') +const model = require('../../src/models') +const logger = require('../../src/common/logger') + +const payments = [] +for (let i = 0; i < 1000; i++) { + payments.push({ + id: uuid(), + workPeriodId: data.WorkPeriods[_.random(3)].id, + amount: _.round(_.random(1000, true), 2), + status: 'scheduled', + billingAccountId: data.ResourceBooking.billingAccountId, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + updatedBy: null, + createdAt: `2021-05-19T21:3${i % 10}:46.507Z`, + updatedAt: '2021-05-19T21:33:46.507Z' + }) +} + +/** + * Clear old demo data + */ +async function clearData () { + const workPeriodIds = _.join(_.map(data.WorkPeriods, w => `'${w.id}'`), ',') + await model.PaymentScheduler.destroy({ + where: { + workPeriodPaymentId: { + [model.Sequelize.Op.in]: [ + model.sequelize.literal(`select id from ${config.DB_SCHEMA_NAME}.work_period_payments where work_period_id in (${workPeriodIds})`) + ] + } + }, + force: true + }) + await model.WorkPeriodPayment.destroy({ + where: { + workPeriodId: _.map(data.WorkPeriods, 'id') + }, + force: true + }) + await model.WorkPeriod.destroy({ + where: { + id: _.map(data.WorkPeriods, 'id') + }, + force: true + }) + await model.ResourceBooking.destroy({ + where: { + id: data.ResourceBooking.id + }, + force: true + }) + await model.Job.destroy({ + where: { + id: data.Job.id + }, + force: true + }) +} + +/** + * Insert payment scheduler demo data + */ +async function insertPaymentSchedulerDemoData () { + logger.info({ component: 'payment-scheduler-demo-data', context: 'insertPaymentSchedulerDemoData', message: 'Starting to remove demo data if exists' }) + await clearData() + logger.info({ component: 'payment-scheduler-demo-data', context: 'insertPaymentSchedulerDemoData', message: 'Data cleared' }) + await model.Job.create(data.Job) + logger.info({ component: 'payment-scheduler-demo-data', context: 'insertPaymentSchedulerDemoData', message: `Job ${data.Job.id} created` }) + await model.ResourceBooking.create(data.ResourceBooking) + logger.info({ component: 'payment-scheduler-demo-data', context: 'insertPaymentSchedulerDemoData', message: `ResourceBooking: ${data.ResourceBooking.id} create` }) + await model.WorkPeriod.bulkCreate(data.WorkPeriods) + logger.info({ component: 'payment-scheduler-demo-data', context: 'insertPaymentSchedulerDemoData', message: `WorkPeriods: ${_.map(data.WorkPeriods, 'id')} created` }) + await model.WorkPeriodPayment.bulkCreate(payments) + logger.info({ component: 'payment-scheduler-demo-data', context: 'insertPaymentSchedulerDemoData', message: `${payments.length} of WorkPeriodPayments scheduled` }) +} + +insertPaymentSchedulerDemoData() diff --git a/src/bootstrap.js b/src/bootstrap.js index 2999f131..ce215169 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -16,12 +16,12 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') -Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied','rejected-pre-screen','skills-test','skills-test','phone-screen','job-closed') +Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed') Joi.title = () => Joi.string().max(128) Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) Joi.interviewStatus = () => Joi.string().valid(...allowedInterviewStatuses) -Joi.workPeriodPaymentStatus = () => Joi.string().valid('completed', 'cancelled') +Joi.workPeriodPaymentStatus = () => Joi.string().valid(..._.values(constants.WorkPeriodPaymentStatus)) // Empty string is not allowed by Joi by default and must be enabled with allow(''). // See https://joi.dev/api/?v=17.3.0#string fro details why it's like this. // In many cases we would like to allow empty string to make it easier to create UI for editing data. @@ -51,7 +51,7 @@ const paymentProcessingSwitchSchema = Joi.string().label('PAYMENT_PROCESSING_SWI ...Object.values(constants.PaymentProcessingSwitch) ) try { - Joi.attempt(config.PAYMENT_PROCESSING_SWITCH, paymentProcessingSwitchSchema) + Joi.attempt(config.PAYMENT_PROCESSING.SWITCH, paymentProcessingSwitchSchema) } catch (err) { console.error(err.message) process.exit(1) diff --git a/src/common/helper.js b/src/common/helper.js index f7bcb148..65704098 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -151,6 +151,16 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { challengeId: { type: 'keyword' }, amount: { type: 'float' }, status: { type: 'keyword' }, + statusDetails: { + type: 'nested', + properties: { + errorMessage: { type: 'text' }, + errorCode: { type: 'integer' }, + retry: { type: 'integer' }, + step: { type: 'keyword' }, + challengeId: { type: 'keyword' } + } + }, billingAccountId: { type: 'integer' }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, @@ -202,6 +212,16 @@ async function promptUser (promptQuery, cb) { }) } +/** + * Sleep for a given number of milliseconds. + * + * @param {Number} milliseconds the sleep time + * @returns {undefined} + */ +async function sleep (milliseconds) { + return new Promise((resolve) => setTimeout(resolve, milliseconds)) +} + /** * Create index in elasticsearch * @param {Object} index the index name @@ -1288,6 +1308,26 @@ async function createChallenge (data, token) { return challenge } +/** + * Get a challenge + * + * @param {Object} data challenge data + * @returns {Object} the challenge + */ +async function getChallenge (challengeId) { + const token = await getM2MToken() + const url = `${config.TC_API}/challenges/${challengeId}` + localLogger.debug({ context: 'getChallenge', message: `EndPoint: GET ${url}` }) + const { body: challenge, status: httpStatus } = await request + .get(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + localLogger.debug({ context: 'getChallenge', message: `Status Code: ${httpStatus}` }) + localLogger.debug({ context: 'getChallenge', message: `Response Body: ${JSON.stringify(challenge)}` }) + return challenge +} + /** * Update a challenge * @@ -1339,6 +1379,35 @@ async function createChallengeResource (data, token) { return resource } +/** + * + * @param {String} challengeId the challenge id + * @param {String} memberHandle the member handle + * @param {String} roleId the role id + * @returns {Object} the resource + */ +async function getChallengeResource (challengeId, memberHandle, roleId) { + const token = await getM2MToken() + const url = `${config.TC_API}/resources?challengeId=${challengeId}&memberHandle=${memberHandle}&roleId=${roleId}` + localLogger.debug({ context: 'createChallengeResource', message: `EndPoint: POST ${url}` }) + try { + const { body: resource, status: httpStatus } = await request + .get(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + localLogger.debug({ context: 'getChallengeResource', message: `Status Code: ${httpStatus}` }) + localLogger.debug({ context: 'getChallengeResource', message: `Response Body: ${JSON.stringify(resource)}` }) + return resource[0] + } catch (err) { + if (err.status === 404) { + localLogger.debug({ context: 'getChallengeResource', message: `Status Code: ${err.status}` }) + } else { + throw err + } + } +} + /** * Populates workPeriods from start and end date of resource booking * @param {Date} start start date of the resource booking @@ -1418,6 +1487,7 @@ async function substituteStringByObject (string, object) { module.exports = { getParamFromCliArgs, promptUser, + sleep, createIndex, deleteIndex, indexBulkDataToES, @@ -1462,8 +1532,10 @@ module.exports = { deleteProjectMember, getUserAttributeValue, createChallenge, + getChallenge, updateChallenge, createChallengeResource, + getChallengeResource, extractWorkPeriods, getUserByHandle, substituteStringByObject diff --git a/src/controllers/WorkPeriodPaymentController.js b/src/controllers/WorkPeriodPaymentController.js index 93f5c046..58352deb 100644 --- a/src/controllers/WorkPeriodPaymentController.js +++ b/src/controllers/WorkPeriodPaymentController.js @@ -20,7 +20,7 @@ async function getWorkPeriodPayment (req, res) { * @param res the response */ async function createWorkPeriodPayment (req, res) { - res.send(await service.createWorkPeriodPayment(req.authUser, req.body, { paymentProcessingSwitch: config.PAYMENT_PROCESSING_SWITCH })) + res.send(await service.createWorkPeriodPayment(req.authUser, req.body, { paymentProcessingSwitch: config.PAYMENT_PROCESSING.SWITCH })) } /** diff --git a/src/models/PaymentScheduler.js b/src/models/PaymentScheduler.js new file mode 100644 index 00000000..7fd171aa --- /dev/null +++ b/src/models/PaymentScheduler.js @@ -0,0 +1,107 @@ +const { Sequelize, Model } = require('sequelize') +const config = require('config') +const errors = require('../common/errors') + +module.exports = (sequelize) => { + class PaymentScheduler extends Model { + /** + * Create association between models + * @param {Object} models the database models + */ + static associate (models) { + PaymentScheduler.belongsTo(models.WorkPeriodPayment, { foreignKey: 'workPeriodPaymentId' }) + } + + /** + * Get payment scheduler by id + * @param {String} id the payment scheduler id + * @returns {PaymentScheduler} the payment scheduler instance + */ + static async findById (id) { + const paymentScheduler = await PaymentScheduler.findOne({ + where: { + id + } + }) + if (!paymentScheduler) { + throw new errors.NotFoundError(`id: ${id} "paymentScheduler" doesn't exists`) + } + return paymentScheduler + } + } + PaymentScheduler.init( + { + id: { + type: Sequelize.UUID, + primaryKey: true, + allowNull: false, + defaultValue: Sequelize.UUIDV4 + }, + challengeId: { + field: 'challenge_id', + type: Sequelize.UUID, + allowNull: false + }, + workPeriodPaymentId: { + field: 'work_period_payment_id', + type: Sequelize.UUID, + allowNull: false + }, + step: { + type: Sequelize.INTEGER, + allowNull: false + }, + status: { + type: Sequelize.ENUM( + 'in-progress', + 'completed', + 'failed' + ), + allowNull: false + }, + userId: { + field: 'user_id', + type: Sequelize.BIGINT + }, + userHandle: { + field: 'user_handle', + type: Sequelize.STRING, + allowNull: false + }, + 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: 'payment_schedulers', + paranoid: true, + deletedAt: 'deletedAt', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + timestamps: true, + defaultScope: { + attributes: { + exclude: ['deletedAt'] + } + }, + hooks: { + afterCreate: (paymentScheduler) => { + delete paymentScheduler.dataValues.deletedAt + } + } + } + ) + + return PaymentScheduler +} diff --git a/src/models/WorkPeriodPayment.js b/src/models/WorkPeriodPayment.js index 3683faf0..349fa653 100644 --- a/src/models/WorkPeriodPayment.js +++ b/src/models/WorkPeriodPayment.js @@ -1,6 +1,8 @@ const { Sequelize, Model } = require('sequelize') +const _ = require('lodash') const config = require('config') const errors = require('../common/errors') +const { WorkPeriodPaymentStatus } = require('../../app-constants') module.exports = (sequelize) => { class WorkPeriodPayment extends Model { @@ -44,19 +46,19 @@ module.exports = (sequelize) => { }, challengeId: { field: 'challenge_id', - type: Sequelize.UUID, - allowNull: false + type: Sequelize.UUID }, amount: { type: Sequelize.DOUBLE }, status: { - type: Sequelize.ENUM( - 'completed', - 'cancelled' - ), + type: Sequelize.ENUM(..._.values(WorkPeriodPaymentStatus)), allowNull: false }, + statusDetails: { + field: 'status_details', + type: Sequelize.JSONB + }, billingAccountId: { field: 'billing_account_id', type: Sequelize.BIGINT diff --git a/src/services/InterviewService.js b/src/services/InterviewService.js index 10a065f4..a69a788c 100644 --- a/src/services/InterviewService.js +++ b/src/services/InterviewService.js @@ -241,8 +241,8 @@ async function requestInterview (currentUser, jobCandidateId, interview) { 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] + var foundGuestMember = _.find(guestMembers, function (guestMember) { return guestEmail === guestMember.email }) + return (foundGuestMember !== undefined) ? `${foundGuestMember.firstName} ${foundGuestMember.lastName}` : guestEmail.split('@')[0] }) try { diff --git a/src/services/PaymentSchedulerService.js b/src/services/PaymentSchedulerService.js new file mode 100644 index 00000000..c0607eab --- /dev/null +++ b/src/services/PaymentSchedulerService.js @@ -0,0 +1,334 @@ +const _ = require('lodash') +const config = require('config') +const moment = require('moment') +const models = require('../models') +const { getV3MemberDetailsByHandle, getChallenge, getChallengeResource, sleep, postEvent } = require('../common/helper') +const logger = require('../common/logger') +const { createChallenge, addResourceToChallenge, activateChallenge, closeChallenge } = require('./PaymentService') +const { ChallengeStatus, PaymentProcessingSwitch } = require('../../app-constants') + +const WorkPeriodPayment = models.WorkPeriodPayment +const WorkPeriod = models.WorkPeriod +const PaymentScheduler = models.PaymentScheduler +const { + SWITCH, BATCH_SIZE, IN_PROGRESS_EXPIRED, MAX_RETRY_COUNT, RETRY_BASE_DELAY, RETRY_MAX_DELAY, PER_REQUEST_MAX_TIME, PER_PAYMENT_MAX_TIME, + PER_MINUTE_PAYMENT_MAX_COUNT, PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT, PER_MINUTE_RESOURCE_REQUEST_MAX_COUNT, + FIX_DELAY_STEP_1_2, FIX_DELAY_STEP_2_3, FIX_DELAY_STEP_3_4 +} = config.PAYMENT_PROCESSING +const processStatus = { + perMin: { + minute: '0:0', + paymentsProcessed: 0, + challengeRequested: 0, + resourceRequested: 0 + }, + perMinThreshold: { + paymentsProcessed: PER_MINUTE_PAYMENT_MAX_COUNT, + challengeRequested: PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT, + resourceRequested: PER_MINUTE_RESOURCE_REQUEST_MAX_COUNT + }, + paymentStartTime: 0, + requestStartTime: 0 +} +const stepEnum = ['start-process', 'create-challenge', 'assign-member', 'activate-challenge', 'get-userId', 'close-challenge'] +const processResult = { + SUCCESS: 'success', + FAIL: 'fail', + SKIP: 'skip' +} + +const localLogger = { + debug: (message, context) => logger.debug({ component: 'PaymentSchedulerService', context, message }), + error: (message, context) => logger.error({ component: 'PaymentSchedulerService', context, message }), + info: (message, context) => logger.info({ component: 'PaymentSchedulerService', context, message }) +} + +/** + * Scheduler process entrance + */ +async function processScheduler () { + // Get the oldest Work Periods Payment records in status "scheduled" and "in-progress", + // the in progress state may be caused by an abnormal shutdown, + // or it may be a normal record that is still being processed + const workPeriodPaymentList = await WorkPeriodPayment.findAll({ where: { status: ['in-progress', 'scheduled'] }, order: [['status', 'desc'], ['createdAt']], limit: BATCH_SIZE }) + localLogger.info(`start processing ${workPeriodPaymentList.length} of payments`, 'processScheduler') + const failIds = [] + const skipIds = [] + for (const workPeriodPayment of workPeriodPaymentList) { + const result = await processPayment(workPeriodPayment) + if (result === processResult.FAIL) { + failIds.push(workPeriodPayment.id) + } else if (result === processResult.SKIP) { + skipIds.push(workPeriodPayment.id) + } + } + localLogger.info(`process end. ${workPeriodPaymentList.length - failIds.length - skipIds.length} of payments processed successfully`, 'processScheduler') + if (!_.isEmpty(skipIds)) { + localLogger.info(`payments: ${_.join(skipIds, ',')} are processing by other processor`, 'processScheduler') + } + if (!_.isEmpty(failIds)) { + localLogger.error(`payments: ${_.join(failIds, ',')} are processed failed`, 'processScheduler') + } +} + +/** + * Process a record of payment + * @param {Object} workPeriodPayment the work period payment + * @returns {String} process result + */ +async function processPayment (workPeriodPayment) { + processStatus.paymentStartTime = Date.now() + let paymentScheduler + if (workPeriodPayment.status === 'in-progress') { + paymentScheduler = await PaymentScheduler.findOne({ where: { workPeriodPaymentId: workPeriodPayment.id, status: 'in-progress' } }) + + // If the in-progress record has not expired, it is considered to be being processed by other processes + if (paymentScheduler && moment(paymentScheduler.updatedAt).add(moment.duration(IN_PROGRESS_EXPIRED)).isAfter(moment())) { + localLogger.info(`workPeriodPayment: ${workPeriodPayment.id} is being processed by other processor`, 'processPayment') + return processResult.SKIP + } + } else { + const oldValue = workPeriodPayment.toJSON() + const updated = await workPeriodPayment.update({ status: 'in-progress' }) + // Update the modified status to es + await postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue }) + } + // Check whether the number of processed records per minute exceeds the specified number, if it exceeds, wait for the next minute before processing + await checkWait(stepEnum[0]) + localLogger.info(`Processing workPeriodPayment ${workPeriodPayment.id}`, 'processPayment') + + const workPeriod = await WorkPeriod.findById(workPeriodPayment.workPeriodId) + try { + if (!paymentScheduler) { + // 1. create challenge + const challengeId = await withRetry(createChallenge, [getCreateChallengeParam(workPeriod, workPeriodPayment)], validateError, stepEnum[1]) + paymentScheduler = await PaymentScheduler.create({ challengeId, step: 1, workPeriodPaymentId: workPeriodPayment.id, userHandle: workPeriod.userHandle, status: 'in-progress' }) + } else { + // If the paymentScheduler already exists, it means that this is a record caused by an abnormal shutdown + await setPaymentSchedulerStep(paymentScheduler) + } + // Start from unprocessed step, perform the process step by step + while (paymentScheduler.step < 5) { + await processStep(paymentScheduler) + } + + const oldValue = workPeriodPayment.toJSON() + // 5. update wp and save it should only update already existent Work Period Payment record with created "challengeId" and "status=completed". + const updated = await workPeriodPayment.update({ challengeId: paymentScheduler.challengeId, status: 'completed' }) + // Update the modified status to es + await postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue }) + + await paymentScheduler.update({ step: 5, userId: paymentScheduler.userId, status: 'completed' }) + + localLogger.info(`Processed workPeriodPayment ${workPeriodPayment.id} successfully`, 'processPayment') + return processResult.SUCCESS + } catch (err) { + logger.logFullError(err, { component: 'PaymentSchedulerService', context: 'processPayment' }) + const statusDetails = { errorMessage: err.message, errorCode: _.get(err, 'status', -1), retry: _.get(err, 'retry', -1), step: _.get(err, 'step'), challengeId: paymentScheduler ? paymentScheduler.challengeId : null } + const oldValue = workPeriodPayment.toJSON() + // If payment processing failed Work Periods Payment "status" should be changed to "failed" and populate "statusDetails" field with error details in JSON format. + const updated = await workPeriodPayment.update({ statusDetails, status: 'failed' }) + // Update the modified status to es + await postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue }) + + if (paymentScheduler) { + await paymentScheduler.update({ step: 5, userId: paymentScheduler.userId, status: 'failed' }) + } + localLogger.error(`Processed workPeriodPayment ${workPeriodPayment.id} failed`, 'processPayment') + return processResult.FAIL + } +} + +/** + * Perform a specific step in the process + * @param {Object} paymentScheduler the payment scheduler + */ +async function processStep (paymentScheduler) { + if (paymentScheduler.step === 1) { + // 2. assign member to the challenge + await withRetry(addResourceToChallenge, [paymentScheduler.challengeId, paymentScheduler.userHandle], validateError, stepEnum[2]) + paymentScheduler.step = 2 + } else if (paymentScheduler.step === 2) { + // 3. active the challenge + await withRetry(activateChallenge, [paymentScheduler.challengeId], validateError, stepEnum[3]) + paymentScheduler.step = 3 + } else if (paymentScheduler.step === 3) { + // 4.1. get user id + const { userId } = await withRetry(getV3MemberDetailsByHandle, [paymentScheduler.userHandle], validateError, stepEnum[4]) + paymentScheduler.userId = userId + paymentScheduler.step = 4 + } else if (paymentScheduler.step === 4) { + // 4.2. close the challenge + await withRetry(closeChallenge, [paymentScheduler.challengeId, paymentScheduler.userId, paymentScheduler.userHandle], validateError, stepEnum[5]) + paymentScheduler.step = 5 + } +} + +/** + * Set the scheduler actual step + * @param {Object} paymentScheduler the scheduler object + */ +async function setPaymentSchedulerStep (paymentScheduler) { + const challenge = await getChallenge(paymentScheduler.challengeId) + if (challenge.status === ChallengeStatus.COMPLETED) { + paymentScheduler.step = 5 + } else if (challenge.status === ChallengeStatus.ACTIVE) { + paymentScheduler.step = 3 + } else { + const resource = await getChallengeResource(paymentScheduler.challengeId, paymentScheduler.userHandle, config.ROLE_ID_SUBMITTER) + if (resource) { + paymentScheduler.step = 2 + } else { + paymentScheduler.step = 1 + } + } + // The main purpose is updating the updatedAt of payment scheduler to avoid simultaneous processing + await paymentScheduler.update({ step: paymentScheduler.step }) +} + +/** + * Generate the create challenge parameter + * @param {Object} workPeriod the work period + * @param {Object} workPeriodPayment the work period payment + * @returns {Object} the create challenge parameter + */ +function getCreateChallengeParam (workPeriod, workPeriodPayment) { + return { + projectId: workPeriod.projectId, + userHandle: workPeriod.userHandle, + amount: workPeriodPayment.amount, + name: `TaaS Payment - ${workPeriod.userHandle} - Week Ending ${moment(workPeriod.endDate).format('D/M/YYYY')}`, + description: `TaaS Payment - ${workPeriod.userHandle} - Week Ending ${moment(workPeriod.endDate).format('D/M/YYYY')}`, + billingAccountId: workPeriodPayment.billingAccountId + } +} + +/** + * Before each step is processed, wait for the corresponding time + * @param {String} step the step name + * @param {Number} tryCount the try count + */ +async function checkWait (step, tryCount) { + // When calculating the retry time later, we need to subtract the time that has been waited before + let lapse = 0 + if (step === stepEnum[0]) { + lapse += await checkPerMinThreshold('paymentsProcessed') + } else if (step === stepEnum[1]) { + await checkPerMinThreshold('challengeRequested') + } else if (step === stepEnum[2]) { + // Only when tryCount = 0, it comes from the previous step, and it is necessary to wait for a fixed time + if (FIX_DELAY_STEP_1_2 > 0 && tryCount === 0) { + await sleep(FIX_DELAY_STEP_1_2) + } + lapse += await checkPerMinThreshold('resourceRequested') + } else if (step === stepEnum[3]) { + // Only when tryCount = 0, it comes from the previous step, and it is necessary to wait for a fixed time + if (FIX_DELAY_STEP_2_3 > 0 && tryCount === 0) { + await sleep(FIX_DELAY_STEP_2_3) + } + lapse += await checkPerMinThreshold('challengeRequested') + } else if (step === stepEnum[5]) { + // Only when tryCount = 0, it comes from the previous step, and it is necessary to wait for a fixed time + if (FIX_DELAY_STEP_3_4 > 0 && tryCount === 0) { + await sleep(FIX_DELAY_STEP_3_4) + } + lapse += await checkPerMinThreshold('challengeRequested') + } + + if (tryCount > 0) { + // exponential backoff and do not exceed the maximum retry delay + const retryDelay = Math.min(RETRY_BASE_DELAY * Math.pow(2, tryCount), RETRY_MAX_DELAY) + await sleep(retryDelay - lapse) + } +} + +/** + * Determine whether the number of records processed every minute exceeds the specified number, if it exceeds, wait for the next minute + * @param {String} key the min threshold key + * @returns {Number} wait time + */ +async function checkPerMinThreshold (key) { + const mt = moment() + const min = mt.format('h:m') + let waitMs = 0 + if (processStatus.perMin.minute === min) { + if (processStatus.perMin[key] >= processStatus.perMinThreshold[key]) { + waitMs = (60 - mt.seconds()) * 1000 + localLogger.info(`The number of records of ${key} processed per minute reaches ${processStatus.perMinThreshold[key]}, and it need to wait for ${60 - mt.seconds()} seconds until the next minute`) + await sleep(waitMs) + processStatus.perMin = { + minute: moment().format('h:m'), + paymentsProcessed: 0, + challengeRequested: 0, + resourceRequested: 0 + } + } + } else { + processStatus.perMin = { + minute: min, + paymentsProcessed: 0, + challengeRequested: 0, + resourceRequested: 0 + } + } + processStatus.perMin[key]++ + return waitMs +} + +/** + * Determine whether it can try again + * @param {Object} err the process error + * @returns {Boolean} + */ +function validateError (err) { + return !err.status || err.status >= 500 +} + +/** + * Execute the function, if an exception occurs, retry according to the conditions + * @param {Function} func the main function + * @param {Array} argArr the args of main function + * @param {Function} predictFunc the determine error function + * @param {String} step the step name + * @returns the result of main function + */ +async function withRetry (func, argArr, predictFunc, step) { + let tryCount = 0 + processStatus.requestStartTime = Date.now() + while (true) { + await checkWait(step, tryCount) + tryCount++ + try { + // mock code + if (SWITCH === PaymentProcessingSwitch.OFF) { + if (step === stepEnum[1]) { + return '00000000-0000-0000-0000-000000000000' + } else if (step === stepEnum[4]) { + return { userId: 100001 } + } + return + } else { + // Execute the main function + const result = await func(...argArr) + return result + } + } catch (err) { + const now = Date.now() + // The following is the case of not retrying: + // 1. The number of retries exceeds the configured number + // 2. The thrown error does not meet the retry conditions + // 3. The request execution time exceeds the configured time + // 4. The processing time of the payment record exceeds the configured time + if (tryCount > MAX_RETRY_COUNT || !predictFunc(err) || now - processStatus.requestStartTime > PER_REQUEST_MAX_TIME || now - processStatus.paymentStartTime > PER_PAYMENT_MAX_TIME) { + err.retry = tryCount + err.step = step + throw err + } + localLogger.info(`execute ${step} with error: ${err.message}, retry...`, 'withRetry') + } + } +} + +module.exports = { + processScheduler +} diff --git a/src/services/PaymentService.js b/src/services/PaymentService.js index d06ad671..93d04e41 100644 --- a/src/services/PaymentService.js +++ b/src/services/PaymentService.js @@ -39,7 +39,8 @@ 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, options.userHandle, token) + const { userId } = await helper.getV3MemberDetailsByHandle(options.userHandle) + const completedChallenge = await closeChallenge(challengeId, userId, options.userHandle, token) return completedChallenge } @@ -117,6 +118,13 @@ async function addResourceToChallenge (id, handle, token) { await helper.createChallengeResource(body, token) localLogger.info({ context: 'addResourceToChallenge', message: `${handle} added to challenge ${id}` }) } catch (err) { + if (err.status === 409) { + const resource = await helper.getChallengeResource(id, handle, config.ROLE_ID_SUBMITTER) + if (resource) { + localLogger.info({ context: 'addResourceToChallenge', message: `${handle} exists in challenge ${id}` }) + return + } + } localLogger.error({ context: 'addResourceToChallenge', message: `Status Code: ${err.status}` }) localLogger.error({ context: 'addResourceToChallenge', message: err.response.text }) throw err @@ -137,6 +145,13 @@ async function activateChallenge (id, token) { await helper.updateChallenge(id, body, token) localLogger.info({ context: 'activateChallenge', message: `Challenge ${id} is activated successfully.` }) } catch (err) { + if (err.status >= 500) { + const challenge = await helper.getChallenge(id) + if (_.includes([constants.ChallengeStatus.ACTIVE, constants.ChallengeStatus.COMPLETED], challenge.status)) { + localLogger.info({ context: 'activateChallenge', message: `the status of Challenge ${id} had been ${challenge.status}.` }) + return + } + } localLogger.error({ context: 'activateChallenge', message: `Status Code: ${err.status}` }) localLogger.error({ context: 'activateChallenge', message: err.response.text }) throw err @@ -146,14 +161,14 @@ async function activateChallenge (id, token) { /** * closes the topcoder challenge * @param {String} id the challenge id + * @param {String} userId the user id * @param {String} userHandle the user handle * @param {String} token m2m token * @returns {Object} the closed challenge */ -async function closeChallenge (id, userHandle, token) { +async function closeChallenge (id, userId, userHandle, token) { localLogger.info({ context: 'closeChallenge', message: `Closing challenge ${id}` }) try { - const { userId } = await helper.getV3MemberDetailsByHandle(userHandle) const body = { status: constants.ChallengeStatus.COMPLETED, winners: [{ @@ -166,6 +181,13 @@ async function closeChallenge (id, userHandle, token) { localLogger.info({ context: 'closeChallenge', message: `Challenge ${id} is closed successfully.` }) return response } catch (err) { + if (err.status >= 500) { + const challenge = await helper.getChallenge(id) + if (constants.ChallengeStatus.COMPLETED === challenge.status) { + localLogger.info({ context: 'activateChallenge', message: `the status of Challenge ${id} had been ${challenge.status}.` }) + return challenge + } + } localLogger.error({ context: 'closeChallenge', message: `Status Code: ${err.status}` }) localLogger.error({ context: 'closeChallenge', message: err.response.text }) throw err @@ -173,5 +195,9 @@ async function closeChallenge (id, userHandle, token) { } module.exports = { - createPayment + createPayment, + createChallenge, + addResourceToChallenge, + activateChallenge, + closeChallenge } diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index f5c40206..f1758c89 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -472,7 +472,6 @@ async function searchResourceBookings (currentUser, criteria, options = { return criteria.sortOrder = 'desc' } try { - throw new Error('fallback to DB') const esQuery = { index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), _source_includes: queryOpt.include, diff --git a/test/unit/ResourceBookingService.test.js b/test/unit/ResourceBookingService.test.js index 64de1900..1fcced93 100644 --- a/test/unit/ResourceBookingService.test.js +++ b/test/unit/ResourceBookingService.test.js @@ -455,6 +455,9 @@ describe('resourceBooking service test', () => { const stubResourceBookingFindAll = sinon.stub(ResourceBooking, 'findAll').callsFake(async () => { return data.resourceBookingFindAll }) + sinon.stub(ResourceBooking, 'count').callsFake(async () => { + return data.resourceBookingFindAll.length + }) const result = await service.searchResourceBookings(commonData.userWithManagePermission, data.criteria) expect(esClientSearch.calledOnce).to.be.true expect(stubResourceBookingFindAll.calledOnce).to.be.true From 86fe56f29157965351e1699e8ede73afc5389430 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 2 Jun 2021 22:04:20 +0300 Subject: [PATCH 31/86] fix: RB search case insensitive --- src/services/ResourceBookingService.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index dead5398..7be3833e 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -556,8 +556,8 @@ async function searchResourceBookings (currentUser, criteria, options = { return const { body } = await esClient.search(esQuery) const resourceBookings = _.map(body.hits.hits, '_source') // ESClient will return ResourceBookings with it's all nested WorkPeriods - // We re-apply WorkPeriod filters - _.each(workPeriodFilters, (value, key) => { + // We re-apply WorkPeriod filters except userHandle because all WPs share same userHandle + _.each(_.omit(workPeriodFilters, 'workPeriods.userHandle'), (value, key) => { key = key.split('.')[1] _.each(resourceBookings, r => { r.workPeriods = _.filter(r.workPeriods, { [key]: value }) @@ -608,11 +608,16 @@ async function searchResourceBookings (currentUser, criteria, options = { return queryCriteria.include[0].attributes = { exclude: _.map(queryOpt.excludeWP, f => _.split(f, '.')[1]) } } // Apply WorkPeriod filters - _.each(_.pick(criteria, ['workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle', 'workPeriods.paymentStatus']), (value, key) => { + _.each(_.pick(criteria, ['workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.paymentStatus']), (value, key) => { key = key.split('.')[1] queryCriteria.include[0].where[Op.and].push({ [key]: value }) - queryCriteria.include[0].required = true }) + if (criteria['workPeriods.userHandle']) { + queryCriteria.include[0].where[Op.and].push({ userHandle: { [Op.iLike]: criteria['workPeriods.userHandle'] } }) + } + if (queryCriteria.include[0].where[Op.and].length > 0) { + queryCriteria.include[0].required = true + } } // Apply sorting criteria if (!queryOpt.sortByWP) { From 39bd1c0e0e6c9569afde4ffb3ae6f77115788734 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 3 Jun 2021 00:33:27 +0300 Subject: [PATCH 32/86] fix: unable to search and create --- src/common/helper.js | 43 ++++++++++++++++++++++++++++--------- src/services/RoleService.js | 16 ++++++++------ 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index a599fb65..2f01161a 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -221,9 +221,15 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { updatedBy: { type: 'keyword' } } esIndexPropertyMapping[config.get('esConfig.ES_INDEX_ROLE')] = { - name: { type: 'keyword' }, + name: { + type: 'keyword', + normalizer: 'lowercaseNormalizer' + }, description: { type: 'keyword' }, - listOfSkills: { type: 'keyword' }, + listOfSkills: { + type: 'keyword', + normalizer: 'lowercaseNormalizer' + }, rates: { properties: { global: { type: 'integer' }, @@ -1199,6 +1205,24 @@ async function getTopcoderSkills (criteria) { } } +/** + * Function to search and retrive all skills from v5/skills + * - only returns skills from Topcoder Skills Provider defined by `TOPCODER_SKILL_PROVIDER_ID` + * + * @param {Object} criteria the search criteria + * @returns the request result + */ +async function getAllTopcoderSkills (criteria) { + const skills = await getTopcoderSkills(_.assign(criteria, { page: 1, perPage: 100 })) + while (skills.page * skills.perPage <= skills.total) { + const newSkills = await getTopcoderSkills(_.assign(criteria, { page: skills.page + 1, perPage: 100 })) + skills.result = [...skills.result, ...newSkills.result] + skills.page = newSkills.page + skills.total = newSkills.total + } + return skills.result +} + /** * Function to get skill by id * @param {String} skillId the skill Id @@ -1745,16 +1769,15 @@ async function substituteStringByObject (string, object) { return string } - /** * Get tags from tagging service * @param {String} description The challenge description * @returns {Array} array of tags */ async function getTags (description) { - const data = { text: description, extract_confidence: false} - const type = "emsi/internal_no_refresh" - const url = `${config.TC_API}/contest-tagging/${type}`; + const data = { text: description, extract_confidence: false } + const type = 'emsi/internal_no_refresh' + const url = `${config.TC_API}/contest-tagging/${type}` const res = await request .post(url) .set('Accept', 'application/json') @@ -1762,12 +1785,11 @@ async function getTags (description) { localLogger.debug({ context: 'getTags', - message: `response body: ${JSON.stringify(res.body)}`, - }); - return _.get(res, 'body'); + message: `response body: ${JSON.stringify(res.body)}` + }) + return _.get(res, 'body') } - /** * @param {Object} currentUser the user performing the action * @param {Object} data title of project and any other info @@ -1819,6 +1841,7 @@ module.exports = { getMembers, getProjectById, getTopcoderSkills, + getAllTopcoderSkills, getSkillById, ensureJobById, ensureResourceBookingById, diff --git a/src/services/RoleService.js b/src/services/RoleService.js index 19006f67..bbd2c141 100644 --- a/src/services/RoleService.js +++ b/src/services/RoleService.js @@ -32,18 +32,18 @@ async function _checkUserPermissionForWriteDeleteRole (currentUser) { * @returns {undefined} */ async function _cleanAndValidateSkillNames (skills) { - // remove duplicates, leading and trailing whitespaces, remove empties and convert to lowercase. - const cleanedSkills = _.uniq(_.filter(_.map(skills, skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill))) + // remove duplicates, leading and trailing whitespaces, empties. + const cleanedSkills = _.uniq(_.filter(_.map(skills, skill => _.trim(skill)), skill => !_.isEmpty(skill))) if (cleanedSkills.length > 0) { // search skills if they are exists - const { result } = await helper.getTopcoderSkills({ name: _.join(cleanedSkills, ',') }) + const result = await helper.getAllTopcoderSkills({ name: _.join(cleanedSkills, ',') }) const skillNames = _.map(result, 'name') // find skills that not valid - const unValidSkills = _.differenceWith(cleanedSkills, skillNames, (a, b) => _.toLower(a) === _.toLower(b)) + const unValidSkills = _.differenceBy(cleanedSkills, skillNames, _.toLower) if (unValidSkills.length > 0) { throw new errors.BadRequestError(`skills: "${unValidSkills}" are not valid`) } - return cleanedSkills + return _.intersectionBy(skillNames, cleanedSkills, _.toLower) } else { return null } @@ -232,7 +232,7 @@ deleteRole.schema = Joi.object().keys({ */ async function searchRoles (currentUser, criteria) { // clean skill names and convert into an array - criteria.skillsList = _.filter(_.map(_.split(_.trim(criteria.skillsList), ','), skill => _.toLower(_.trim(skill))), skill => !_.isEmpty(skill)) + criteria.skillsList = _.filter(_.map(_.split(criteria.skillsList, ','), skill => _.trim(skill)), skill => !_.isEmpty(skill)) try { const esQuery = { index: config.get('esConfig.ES_INDEX_ROLE'), @@ -274,7 +274,9 @@ async function searchRoles (currentUser, criteria) { const filter = { [Op.and]: [] } // Apply skill name filters. listOfSkills array should include all skills provided in criteria. if (criteria.skillsList) { - filter[Op.and].push({ listOfSkills: { [Op.contains]: criteria.skillsList } }) + _.each(criteria.skillsList, skill => { + filter[Op.and].push(models.Sequelize.literal(`LOWER('${skill}') in (SELECT lower(x) FROM unnest("list_of_skills"::text[]) x)`)) + }) } // Apply name filter, allow partial match and ignore case if (criteria.keyword) { From 382d708d428b1797ec17e2af16768dc34bda3399 Mon Sep 17 00:00:00 2001 From: yoution Date: Thu, 3 Jun 2021 07:22:15 +0800 Subject: [PATCH 33/86] fix: resolve postman conflict --- ...coder-bookings-api.postman_collection.json | 118 +++++++++--------- 1 file changed, 57 insertions(+), 61 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index fec4d0d6..c1233474 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "ca01f845-9ba8-473d-b005-fc200ac3cd39", + "_postman_id": "2c9dbe94-39f9-4a01-97e4-70f781fc1364", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -19819,6 +19819,44 @@ }, "response": [] }, + { + "name": "POST /taas-teams/getSkillsByJobDescription", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"description\": \"nodejs react c++ hello\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "getSkillsByJobDescription" + ] + } + }, + "response": [] + }, { "name": "GET /taas-teams/:id/members", "request": { @@ -22554,65 +22592,23 @@ ] }, { - "name": "POST /taas-teams/getSkillsByJobDescription", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_member}}" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"description\": \"nodejs react c++ hello\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", - "host": [ - "{{URL}}" - ], - "path": [ - "taas-teams", - "getSkillsByJobDescription" - ] - } - }, - "response": [] - }, - { - "name": "POST /taas-teams/email - member-issue-report", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_member}}" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"template\": \"member-issue-report\",\n \"data\": {\n \"projectName\": \"TaaS Project Name\",\n \"projectId\": 12345,\n \"userHandle\": \"pshah_manager\",\n \"reportText\": \"I have issue with ...\"\n }\n}", - "options": { - "raw": { - "language": "json" + "name": "Delete Role", + "item": [ + { + "name": "delete role with connect user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } } ], "request": { @@ -32849,4 +32845,4 @@ ] } ] -} \ No newline at end of file +} From ac7f0e449021bb9db0a1c1de3ea9d9d8f51739dc Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 3 Jun 2021 23:15:49 +0300 Subject: [PATCH 34/86] add new topic: action.retry --- README.md | 3 ++- config/default.js | 2 ++ local/kafka-client/topics.txt | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aa36c621..30ac169b 100644 --- a/README.md +++ b/README.md @@ -103,10 +103,11 @@ tc-taas-es-processor | 2021-04-09T21:20:21.469Z DEBUG no-kafka-client Subscribed to taas.workperiodpayment.update:0 offset 0 leader kafka:9093 tc-taas-es-processor | 2021-04-09T21:20:21.470Z DEBUG no-kafka-client Subscribed to taas.workperiodpayment.delete:0 offset 0 leader kafka:9093 tc-taas-es-processor | 2021-04-09T21:20:21.471Z DEBUG no-kafka-client Subscribed to taas.workperiodpayment.create:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-04-09T21:20:21.472Z DEBUG no-kafka-client Subscribed to taas.action.retry:0 offset 0 leader kafka:9093 tc-taas-es-processor | 2021-04-09T21:20:21.473Z DEBUG no-kafka-client Subscribed to taas.job.update:0 offset 0 leader kafka:9093 tc-taas-es-processor | 2021-04-09T21:20:21.474Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.update:0 offset 0 leader kafka:9093 tc-taas-es-processor | [2021-04-09T21:20:21.475Z] app INFO : Initialized....... - tc-taas-es-processor | [2021-04-09T21:20:21.479Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete,taas.workperiod.create,taas.workperiod.update,taas.workperiod.delete,taas.workperiodpayment.create,taas.workperiodpayment.update,taas.interview.requested,taas.interview.update,taas.interview.bulkUpdate,taas.role.requested,taas.role.update,taas.role.delete + tc-taas-es-processor | [2021-04-09T21:20:21.479Z] app INFO : common.error.reporting,taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete,taas.workperiod.create,taas.workperiod.update,taas.workperiod.delete,taas.workperiodpayment.create,taas.workperiodpayment.update,taas.interview.requested,taas.interview.update,taas.interview.bulkUpdate,taas.role.requested,taas.role.update,taas.role.delete,taas.action.retry tc-taas-es-processor | [2021-04-09T21:20:21.480Z] app INFO : Kick Start....... tc-taas-es-processor | ********** Topcoder Health Check DropIn listening on port 3001 tc-taas-es-processor | Topcoder Health Check DropIn started and ready to roll diff --git a/config/default.js b/config/default.js index 0af05190..52ba3ecc 100644 --- a/config/default.js +++ b/config/default.js @@ -140,6 +140,8 @@ module.exports = { TAAS_ROLE_UPDATE_TOPIC: process.env.TAAS_ROLE_UPDATE_TOPIC || 'taas.role.update', // the delete role entity Kafka message topic TAAS_ROLE_DELETE_TOPIC: process.env.TAAS_ROLE_DELETE_TOPIC || 'taas.role.delete', + // special kafka topics + TAAS_ACTION_RETRY_TOPIC: process.env.TAAS_ACTION_RETRY_TOPIC || 'taas.action.retry', // the Kafka message topic for sending email EMAIL_TOPIC: process.env.EMAIL_TOPIC || 'external.action.email', diff --git a/local/kafka-client/topics.txt b/local/kafka-client/topics.txt index 760c3a82..2611220b 100644 --- a/local/kafka-client/topics.txt +++ b/local/kafka-client/topics.txt @@ -20,3 +20,4 @@ taas.interview.requested taas.interview.update taas.interview.bulkUpdate external.action.email +taas.action.retry From 81772c000ade66b766842a96d9536be144a1a2a0 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 3 Jun 2021 23:25:15 +0300 Subject: [PATCH 35/86] fix: unit tests --- test/unit/ResourceBookingService.test.js | 4 ++-- test/unit/common/ResourceBookingData.js | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/unit/ResourceBookingService.test.js b/test/unit/ResourceBookingService.test.js index 862ef357..7b52bde2 100644 --- a/test/unit/ResourceBookingService.test.js +++ b/test/unit/ResourceBookingService.test.js @@ -447,7 +447,7 @@ describe('resourceBooking service test', () => { expect(esClientSearch.calledOnce).to.be.true expect(result).to.deep.eq(data.result) }) - it('T25:Search resource bookin from DB', async () => { + it('T25:Search resource booking from DB', async () => { const data = testData.T25 const ESClient = commonData.ESClient ESClient.search = () => {} @@ -456,7 +456,7 @@ describe('resourceBooking service test', () => { return data.resourceBookingFindAll }) const stubResourceBookingCount = sinon.stub(ResourceBooking, 'count').callsFake(async () => { - return data.resourceBookingFindAll.length + return data.resourceBookingCount }) const result = await service.searchResourceBookings(commonData.userWithManagePermission, data.criteria) expect(esClientSearch.calledOnce).to.be.true diff --git a/test/unit/common/ResourceBookingData.js b/test/unit/common/ResourceBookingData.js index b296a384..78732e91 100644 --- a/test/unit/common/ResourceBookingData.js +++ b/test/unit/common/ResourceBookingData.js @@ -1229,6 +1229,8 @@ const T25 = { updatedAt: '2021-05-08T18:47:37.268Z' } ], + resourceBookingCount: [{ id: 'fbe133dd-0e36-4d0c-8197-49307b13ce75', count: 1 }, + { id: '60e99790-8da0-4596-badc-29a06feb78a0', count: 1 }], criteria: {}, result: { fromDb: true, From 15a00f410dc4fe4421d03d28e4457ec8816719ea Mon Sep 17 00:00:00 2001 From: xxcxy Date: Sat, 5 Jun 2021 11:10:32 +0800 Subject: [PATCH 36/86] fix some review issues --- src/services/PaymentSchedulerService.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/services/PaymentSchedulerService.js b/src/services/PaymentSchedulerService.js index c0607eab..963f2e95 100644 --- a/src/services/PaymentSchedulerService.js +++ b/src/services/PaymentSchedulerService.js @@ -170,7 +170,9 @@ async function processStep (paymentScheduler) { */ async function setPaymentSchedulerStep (paymentScheduler) { const challenge = await getChallenge(paymentScheduler.challengeId) - if (challenge.status === ChallengeStatus.COMPLETED) { + if (SWITCH === PaymentProcessingSwitch.OFF) { + paymentScheduler.step = 5 + } else if (challenge.status === ChallengeStatus.COMPLETED) { paymentScheduler.step = 5 } else if (challenge.status === ChallengeStatus.ACTIVE) { paymentScheduler.step = 3 @@ -301,6 +303,8 @@ async function withRetry (func, argArr, predictFunc, step) { try { // mock code if (SWITCH === PaymentProcessingSwitch.OFF) { + // without actual API calls by adding delay (for example 1 second for each step), to simulate the act + sleep(1000) if (step === stepEnum[1]) { return '00000000-0000-0000-0000-000000000000' } else if (step === stepEnum[4]) { @@ -316,7 +320,7 @@ async function withRetry (func, argArr, predictFunc, step) { const now = Date.now() // The following is the case of not retrying: // 1. The number of retries exceeds the configured number - // 2. The thrown error does not meet the retry conditions + // 2. The thrown error does not match the retry conditions // 3. The request execution time exceeds the configured time // 4. The processing time of the payment record exceeds the configured time if (tryCount > MAX_RETRY_COUNT || !predictFunc(err) || now - processStatus.requestStartTime > PER_REQUEST_MAX_TIME || now - processStatus.paymentStartTime > PER_PAYMENT_MAX_TIME) { From abf3e6d83ca2a878e7641709abb5fb671b87f778 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Sat, 5 Jun 2021 22:59:41 +0300 Subject: [PATCH 37/86] role search and make team --- app-constants.js | 2 + config/default.js | 6 +- ...coder-bookings-api.postman_collection.json | 1693 ++++++++++++++++- docs/swagger.yaml | 198 +- ...topcoder-bookings.postman_environment.json | 109 +- ...-06-04-role-search-request-table-create.js | 77 + src/common/helper.js | 31 +- src/controllers/SkillController.js | 20 - src/controllers/TeamController.js | 35 +- src/models/Role.js | 9 + src/models/RoleSearchRequest.js | 110 ++ src/routes/TeamRoutes.js | 20 +- src/services/SkillService.js | 26 - src/services/TeamService.js | 375 +++- 14 files changed, 2618 insertions(+), 93 deletions(-) create mode 100644 migrations/2021-06-04-role-search-request-table-create.js delete mode 100644 src/controllers/SkillController.js create mode 100644 src/models/RoleSearchRequest.js delete mode 100644 src/services/SkillService.js 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 +} From 71f53c77e337f8ea6a19f60a6c6d0d8d5d5adc72 Mon Sep 17 00:00:00 2001 From: dengjun Date: Mon, 7 Jun 2021 20:32:13 +0800 Subject: [PATCH 38/86] adjust jobIds issue --- src/controllers/JobController.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/controllers/JobController.js b/src/controllers/JobController.js index b7cad958..606c690b 100644 --- a/src/controllers/JobController.js +++ b/src/controllers/JobController.js @@ -2,6 +2,7 @@ * Controller for Job endpoints */ const HttpStatus = require('http-status-codes') +const _ = require('lodash') const service = require('../services/JobService') const helper = require('../common/helper') @@ -57,10 +58,8 @@ async function deleteJob (req, res) { * @param res the response */ async function searchJobs (req, res) { - if (req.body && req.body.jobIds) { - req.query.jobIds = req.body.jobIds - } - const result = await service.searchJobs(req.authUser, req.query) + const query = { ...req.query, jobIds: _.get(req, 'body.jobIds', []) } + const result = await service.searchJobs(req.authUser, query) helper.setResHeaders(req, res, result) res.send(result.result) } From 98d9b05a66e4f0c647c0cc0114fff401c20d4806 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 8 Jun 2021 14:22:32 +0300 Subject: [PATCH 39/86] fix: swagger resolve conflict issue --- docs/swagger.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index bc64d85d..94c147b0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2403,11 +2403,7 @@ paths: required: false schema: type: string -<<<<<<< HEAD enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"] -======= - enum: ["completed", "scheduled", "cancelled"] ->>>>>>> 925d7bd8f9933bad9b6a8e3c59b01dbd9246f1ad description: The payment status. responses: "200": @@ -2523,7 +2519,7 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - + /work-period-payments/{id}: get: tags: From 425e62f47749046612af7b6c0f02533f5b77bd17 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 8 Jun 2021 15:28:37 +0300 Subject: [PATCH 40/86] fix: payment amount calculation ref issue #317 --- src/services/WorkPeriodPaymentService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index 59f63720..c6ce3982 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -81,7 +81,7 @@ async function _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking (w if (daysWorked === 0) { workPeriodPayment.amount = 0 } else { - workPeriodPayment.amount = _.round(memberRate * 5 / daysWorked, 2) + workPeriodPayment.amount = _.round(memberRate * daysWorked / 5, 2) } } From cf3dec25eb82233689a986fdb2c048decbdc6934 Mon Sep 17 00:00:00 2001 From: xxcxy Date: Tue, 8 Jun 2021 23:40:46 +0800 Subject: [PATCH 41/86] use payment scheduler step identifiers --- app-constants.js | 10 +++ config/default.js | 12 +-- ...ler-table-add-status-details-to-payment.js | 7 +- src/models/PaymentScheduler.js | 4 +- src/services/PaymentSchedulerService.js | 75 +++++++++---------- 5 files changed, 61 insertions(+), 47 deletions(-) diff --git a/app-constants.js b/app-constants.js index 095a4d07..2c232275 100644 --- a/app-constants.js +++ b/app-constants.js @@ -97,6 +97,15 @@ const PaymentProcessingSwitch = { OFF: 'OFF' } +const PaymentSchedulerStatus = { + START_PROCESS: 'start-process', + CREATE_CHALLENGE: 'create-challenge', + ASSIGN_MEMBER: 'assign-member', + ACTIVATE_CHALLENGE: 'activate-challenge', + GET_USER_ID: 'get-userId', + CLOSE_CHALLENGE: 'close-challenge' +} + module.exports = { UserRoles, FullManagePermissionRoles, @@ -104,5 +113,6 @@ module.exports = { Interviews, ChallengeStatus, WorkPeriodPaymentStatus, + PaymentSchedulerStatus, PaymentProcessingSwitch } diff --git a/config/default.js b/config/default.js index 0e06f2d4..a3418181 100644 --- a/config/default.js +++ b/config/default.js @@ -202,11 +202,11 @@ module.exports = { PER_MINUTE_RESOURCE_REQUEST_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT || 20), // the default step fix delay, unit: ms FIX_DELAY_STEP: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500), - // the fix delay between step one and step two, unit: ms - FIX_DELAY_STEP_1_2: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_1_2 || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500), - // the fix delay between step two and step three, unit: ms - FIX_DELAY_STEP_2_3: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_2_3 || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500), - // the fix delay between step three and step four, unit: ms - FIX_DELAY_STEP_3_4: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_3_4 || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500) + // the fix delay after step of create challenge, unit: ms + FIX_DELAY_STEP_CREATE_CHALLENGE: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_CREATE_CHALLENGE || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500), + // the fix delay after step of assign member, unit: ms + FIX_DELAY_STEP_ASSIGN_MEMBER: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_ASSIGN_MEMBER || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500), + // the fix delay after step of activate challenge, unit: ms + FIX_DELAY_STEP_ACTIVATE_CHALLENGE: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_ACTIVATE_CHALLENGE || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500) } } diff --git a/migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js b/migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js index 40c1596b..5eb2232d 100644 --- a/migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js +++ b/migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js @@ -2,6 +2,7 @@ const config = require('config') const _ = require('lodash') +const { PaymentSchedulerStatus } = require('../app-constants') /** * Create `payment_schedulers` table & relations. @@ -35,7 +36,7 @@ module.exports = { } }, step: { - type: Sequelize.INTEGER, + type: Sequelize.ENUM(_.values(PaymentSchedulerStatus)), allowNull: false }, status: { @@ -87,11 +88,13 @@ module.exports = { down: async (queryInterface, Sequelize) => { const table = { schema: config.DB_SCHEMA_NAME, tableName: 'payment_schedulers' } const statusTypeName = `${table.schema}.enum_${table.tableName}_status` + const stepTypeName = `${table.schema}.enum_${table.tableName}_step` const transaction = await queryInterface.sequelize.transaction() try { await queryInterface.dropTable(table, { transaction }) - // drop enum type for status column + // drop enum type for status and step column await queryInterface.sequelize.query(`DROP TYPE ${statusTypeName}`, { transaction }) + await queryInterface.sequelize.query(`DROP TYPE ${stepTypeName}`, { transaction }) await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'challenge_id', { type: Sequelize.UUID, allowNull: false }, diff --git a/src/models/PaymentScheduler.js b/src/models/PaymentScheduler.js index 7fd171aa..f8f64dfa 100644 --- a/src/models/PaymentScheduler.js +++ b/src/models/PaymentScheduler.js @@ -1,6 +1,8 @@ const { Sequelize, Model } = require('sequelize') const config = require('config') +const _ = require('lodash') const errors = require('../common/errors') +const { PaymentSchedulerStatus } = require('../../app-constants') module.exports = (sequelize) => { class PaymentScheduler extends Model { @@ -48,7 +50,7 @@ module.exports = (sequelize) => { allowNull: false }, step: { - type: Sequelize.INTEGER, + type: Sequelize.ENUM(_.values(PaymentSchedulerStatus)), allowNull: false }, status: { diff --git a/src/services/PaymentSchedulerService.js b/src/services/PaymentSchedulerService.js index 963f2e95..1ce776e9 100644 --- a/src/services/PaymentSchedulerService.js +++ b/src/services/PaymentSchedulerService.js @@ -5,7 +5,7 @@ const models = require('../models') const { getV3MemberDetailsByHandle, getChallenge, getChallengeResource, sleep, postEvent } = require('../common/helper') const logger = require('../common/logger') const { createChallenge, addResourceToChallenge, activateChallenge, closeChallenge } = require('./PaymentService') -const { ChallengeStatus, PaymentProcessingSwitch } = require('../../app-constants') +const { ChallengeStatus, PaymentSchedulerStatus, PaymentProcessingSwitch } = require('../../app-constants') const WorkPeriodPayment = models.WorkPeriodPayment const WorkPeriod = models.WorkPeriod @@ -13,7 +13,7 @@ const PaymentScheduler = models.PaymentScheduler const { SWITCH, BATCH_SIZE, IN_PROGRESS_EXPIRED, MAX_RETRY_COUNT, RETRY_BASE_DELAY, RETRY_MAX_DELAY, PER_REQUEST_MAX_TIME, PER_PAYMENT_MAX_TIME, PER_MINUTE_PAYMENT_MAX_COUNT, PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT, PER_MINUTE_RESOURCE_REQUEST_MAX_COUNT, - FIX_DELAY_STEP_1_2, FIX_DELAY_STEP_2_3, FIX_DELAY_STEP_3_4 + FIX_DELAY_STEP_CREATE_CHALLENGE, FIX_DELAY_STEP_ASSIGN_MEMBER, FIX_DELAY_STEP_ACTIVATE_CHALLENGE } = config.PAYMENT_PROCESSING const processStatus = { perMin: { @@ -30,7 +30,6 @@ const processStatus = { paymentStartTime: 0, requestStartTime: 0 } -const stepEnum = ['start-process', 'create-challenge', 'assign-member', 'activate-challenge', 'get-userId', 'close-challenge'] const processResult = { SUCCESS: 'success', FAIL: 'fail', @@ -94,21 +93,21 @@ async function processPayment (workPeriodPayment) { await postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue }) } // Check whether the number of processed records per minute exceeds the specified number, if it exceeds, wait for the next minute before processing - await checkWait(stepEnum[0]) + await checkWait(PaymentSchedulerStatus.START_PROCESS) localLogger.info(`Processing workPeriodPayment ${workPeriodPayment.id}`, 'processPayment') const workPeriod = await WorkPeriod.findById(workPeriodPayment.workPeriodId) try { if (!paymentScheduler) { // 1. create challenge - const challengeId = await withRetry(createChallenge, [getCreateChallengeParam(workPeriod, workPeriodPayment)], validateError, stepEnum[1]) + const challengeId = await withRetry(createChallenge, [getCreateChallengeParam(workPeriod, workPeriodPayment)], validateError, PaymentSchedulerStatus.CREATE_CHALLENGE) paymentScheduler = await PaymentScheduler.create({ challengeId, step: 1, workPeriodPaymentId: workPeriodPayment.id, userHandle: workPeriod.userHandle, status: 'in-progress' }) } else { // If the paymentScheduler already exists, it means that this is a record caused by an abnormal shutdown await setPaymentSchedulerStep(paymentScheduler) } // Start from unprocessed step, perform the process step by step - while (paymentScheduler.step < 5) { + while (paymentScheduler.step !== PaymentSchedulerStatus.CLOSE_CHALLENGE) { await processStep(paymentScheduler) } @@ -118,7 +117,7 @@ async function processPayment (workPeriodPayment) { // Update the modified status to es await postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue }) - await paymentScheduler.update({ step: 5, userId: paymentScheduler.userId, status: 'completed' }) + await paymentScheduler.update({ step: PaymentSchedulerStatus.CLOSE_CHALLENGE, userId: paymentScheduler.userId, status: 'completed' }) localLogger.info(`Processed workPeriodPayment ${workPeriodPayment.id} successfully`, 'processPayment') return processResult.SUCCESS @@ -132,7 +131,7 @@ async function processPayment (workPeriodPayment) { await postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue }) if (paymentScheduler) { - await paymentScheduler.update({ step: 5, userId: paymentScheduler.userId, status: 'failed' }) + await paymentScheduler.update({ step: PaymentSchedulerStatus.CLOSE_CHALLENGE, userId: paymentScheduler.userId, status: 'failed' }) } localLogger.error(`Processed workPeriodPayment ${workPeriodPayment.id} failed`, 'processPayment') return processResult.FAIL @@ -144,23 +143,23 @@ async function processPayment (workPeriodPayment) { * @param {Object} paymentScheduler the payment scheduler */ async function processStep (paymentScheduler) { - if (paymentScheduler.step === 1) { + if (paymentScheduler.step === PaymentSchedulerStatus.CREATE_CHALLENGE) { // 2. assign member to the challenge - await withRetry(addResourceToChallenge, [paymentScheduler.challengeId, paymentScheduler.userHandle], validateError, stepEnum[2]) - paymentScheduler.step = 2 - } else if (paymentScheduler.step === 2) { + await withRetry(addResourceToChallenge, [paymentScheduler.challengeId, paymentScheduler.userHandle], validateError, PaymentSchedulerStatus.ASSIGN_MEMBER) + paymentScheduler.step = PaymentSchedulerStatus.ASSIGN_MEMBER + } else if (paymentScheduler.step === PaymentSchedulerStatus.ASSIGN_MEMBER) { // 3. active the challenge - await withRetry(activateChallenge, [paymentScheduler.challengeId], validateError, stepEnum[3]) - paymentScheduler.step = 3 - } else if (paymentScheduler.step === 3) { + await withRetry(activateChallenge, [paymentScheduler.challengeId], validateError, PaymentSchedulerStatus.ACTIVATE_CHALLENGE) + paymentScheduler.step = PaymentSchedulerStatus.ACTIVATE_CHALLENGE + } else if (paymentScheduler.step === PaymentSchedulerStatus.ACTIVATE_CHALLENGE) { // 4.1. get user id - const { userId } = await withRetry(getV3MemberDetailsByHandle, [paymentScheduler.userHandle], validateError, stepEnum[4]) + const { userId } = await withRetry(getV3MemberDetailsByHandle, [paymentScheduler.userHandle], validateError, PaymentSchedulerStatus.GET_USER_ID) paymentScheduler.userId = userId - paymentScheduler.step = 4 - } else if (paymentScheduler.step === 4) { + paymentScheduler.step = PaymentSchedulerStatus.GET_USER_ID + } else if (paymentScheduler.step === PaymentSchedulerStatus.GET_USER_ID) { // 4.2. close the challenge - await withRetry(closeChallenge, [paymentScheduler.challengeId, paymentScheduler.userId, paymentScheduler.userHandle], validateError, stepEnum[5]) - paymentScheduler.step = 5 + await withRetry(closeChallenge, [paymentScheduler.challengeId, paymentScheduler.userId, paymentScheduler.userHandle], validateError, PaymentSchedulerStatus.CLOSE_CHALLENGE) + paymentScheduler.step = PaymentSchedulerStatus.CLOSE_CHALLENGE } } @@ -171,17 +170,17 @@ async function processStep (paymentScheduler) { async function setPaymentSchedulerStep (paymentScheduler) { const challenge = await getChallenge(paymentScheduler.challengeId) if (SWITCH === PaymentProcessingSwitch.OFF) { - paymentScheduler.step = 5 + paymentScheduler.step = PaymentSchedulerStatus.CLOSE_CHALLENGE } else if (challenge.status === ChallengeStatus.COMPLETED) { - paymentScheduler.step = 5 + paymentScheduler.step = PaymentSchedulerStatus.CLOSE_CHALLENGE } else if (challenge.status === ChallengeStatus.ACTIVE) { - paymentScheduler.step = 3 + paymentScheduler.step = PaymentSchedulerStatus.ACTIVATE_CHALLENGE } else { const resource = await getChallengeResource(paymentScheduler.challengeId, paymentScheduler.userHandle, config.ROLE_ID_SUBMITTER) if (resource) { - paymentScheduler.step = 2 + paymentScheduler.step = PaymentSchedulerStatus.ASSIGN_MEMBER } else { - paymentScheduler.step = 1 + paymentScheduler.step = PaymentSchedulerStatus.CREATE_CHALLENGE } } // The main purpose is updating the updatedAt of payment scheduler to avoid simultaneous processing @@ -213,26 +212,26 @@ function getCreateChallengeParam (workPeriod, workPeriodPayment) { async function checkWait (step, tryCount) { // When calculating the retry time later, we need to subtract the time that has been waited before let lapse = 0 - if (step === stepEnum[0]) { + if (step === PaymentSchedulerStatus.START_PROCESS) { lapse += await checkPerMinThreshold('paymentsProcessed') - } else if (step === stepEnum[1]) { + } else if (step === PaymentSchedulerStatus.CREATE_CHALLENGE) { await checkPerMinThreshold('challengeRequested') - } else if (step === stepEnum[2]) { + } else if (step === PaymentSchedulerStatus.ASSIGN_MEMBER) { // Only when tryCount = 0, it comes from the previous step, and it is necessary to wait for a fixed time - if (FIX_DELAY_STEP_1_2 > 0 && tryCount === 0) { - await sleep(FIX_DELAY_STEP_1_2) + if (FIX_DELAY_STEP_CREATE_CHALLENGE > 0 && tryCount === 0) { + await sleep(FIX_DELAY_STEP_CREATE_CHALLENGE) } lapse += await checkPerMinThreshold('resourceRequested') - } else if (step === stepEnum[3]) { + } else if (step === PaymentSchedulerStatus.ACTIVATE_CHALLENGE) { // Only when tryCount = 0, it comes from the previous step, and it is necessary to wait for a fixed time - if (FIX_DELAY_STEP_2_3 > 0 && tryCount === 0) { - await sleep(FIX_DELAY_STEP_2_3) + if (FIX_DELAY_STEP_ASSIGN_MEMBER > 0 && tryCount === 0) { + await sleep(FIX_DELAY_STEP_ASSIGN_MEMBER) } lapse += await checkPerMinThreshold('challengeRequested') - } else if (step === stepEnum[5]) { + } else if (step === PaymentSchedulerStatus.CLOSE_CHALLENGE) { // Only when tryCount = 0, it comes from the previous step, and it is necessary to wait for a fixed time - if (FIX_DELAY_STEP_3_4 > 0 && tryCount === 0) { - await sleep(FIX_DELAY_STEP_3_4) + if (FIX_DELAY_STEP_ACTIVATE_CHALLENGE > 0 && tryCount === 0) { + await sleep(FIX_DELAY_STEP_ACTIVATE_CHALLENGE) } lapse += await checkPerMinThreshold('challengeRequested') } @@ -305,9 +304,9 @@ async function withRetry (func, argArr, predictFunc, step) { if (SWITCH === PaymentProcessingSwitch.OFF) { // without actual API calls by adding delay (for example 1 second for each step), to simulate the act sleep(1000) - if (step === stepEnum[1]) { + if (step === PaymentSchedulerStatus.CREATE_CHALLENGE) { return '00000000-0000-0000-0000-000000000000' - } else if (step === stepEnum[4]) { + } else if (step === PaymentSchedulerStatus.GET_USER_ID) { return { userId: 100001 } } return From 211890219c68499445409714fbc5edafbdd2f697 Mon Sep 17 00:00:00 2001 From: xxcxy Date: Wed, 9 Jun 2021 16:09:36 +0800 Subject: [PATCH 42/86] fix scheduler bugs --- src/services/PaymentSchedulerService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/PaymentSchedulerService.js b/src/services/PaymentSchedulerService.js index 1ce776e9..27774fb5 100644 --- a/src/services/PaymentSchedulerService.js +++ b/src/services/PaymentSchedulerService.js @@ -101,7 +101,7 @@ async function processPayment (workPeriodPayment) { if (!paymentScheduler) { // 1. create challenge const challengeId = await withRetry(createChallenge, [getCreateChallengeParam(workPeriod, workPeriodPayment)], validateError, PaymentSchedulerStatus.CREATE_CHALLENGE) - paymentScheduler = await PaymentScheduler.create({ challengeId, step: 1, workPeriodPaymentId: workPeriodPayment.id, userHandle: workPeriod.userHandle, status: 'in-progress' }) + paymentScheduler = await PaymentScheduler.create({ challengeId, step: PaymentSchedulerStatus.CREATE_CHALLENGE, workPeriodPaymentId: workPeriodPayment.id, userHandle: workPeriod.userHandle, status: 'in-progress' }) } else { // If the paymentScheduler already exists, it means that this is a record caused by an abnormal shutdown await setPaymentSchedulerStep(paymentScheduler) From 0ec2d7f90e79d8173b6bdb9e04fbafceb0daffbc Mon Sep 17 00:00:00 2001 From: yoution Date: Wed, 9 Jun 2021 16:04:21 +0800 Subject: [PATCH 43/86] fix: issue #316 --- src/eventHandlers/JobCandidateEventHandler.js | 37 ++++++++++++++----- src/eventHandlers/index.js | 1 + 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/eventHandlers/JobCandidateEventHandler.js b/src/eventHandlers/JobCandidateEventHandler.js index c199cb7d..8780ab08 100644 --- a/src/eventHandlers/JobCandidateEventHandler.js +++ b/src/eventHandlers/JobCandidateEventHandler.js @@ -23,17 +23,25 @@ async function inReviewJob (payload) { }) return } - await JobService.partiallyUpdateJob( - helper.getAuditM2Muser(), - job.id, - { status: 'in-review' } - ).then(result => { - logger.info({ + if (payload.value.status === 'open') { + await JobService.partiallyUpdateJob( + helper.getAuditM2Muser(), + job.id, + { status: 'in-review' } + ).then(result => { + logger.info({ + component: 'JobCandidateEventHandler', + context: 'inReviewJob', + message: `id: ${result.id} job got in-review status.` + }) + }) + } else { + logger.debug({ component: 'JobCandidateEventHandler', context: 'inReviewJob', - message: `id: ${result.id} job got in-review status.` + message: `id: ${payload.value.id} candidate is not in open status` }) - }) + } } /** @@ -46,6 +54,17 @@ async function processCreate (payload) { await inReviewJob(payload) } +/** + * Process job candidate update event. + * + * @param {Object} payload the event payload + * @returns {undefined} + */ +async function processUpdate (payload) { + await inReviewJob(payload) +} + module.exports = { - processCreate + processCreate, + processUpdate } diff --git a/src/eventHandlers/index.js b/src/eventHandlers/index.js index 6e0ec2a8..c88ef929 100644 --- a/src/eventHandlers/index.js +++ b/src/eventHandlers/index.js @@ -14,6 +14,7 @@ const logger = require('../common/logger') const TopicOperationMapping = { [config.TAAS_JOB_UPDATE_TOPIC]: JobEventHandler.processUpdate, [config.TAAS_JOB_CANDIDATE_CREATE_TOPIC]: JobCandidateEventHandler.processCreate, + [config.TAAS_JOB_CANDIDATE_UPDATE_TOPIC]: JobCandidateEventHandler.processUpdate, [config.TAAS_RESOURCE_BOOKING_CREATE_TOPIC]: ResourceBookingEventHandler.processCreate, [config.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC]: ResourceBookingEventHandler.processUpdate, [config.TAAS_RESOURCE_BOOKING_DELETE_TOPIC]: ResourceBookingEventHandler.processDelete, From eb29a8c727277590ad7b651d8b9a30f060b49dcb Mon Sep 17 00:00:00 2001 From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com> Date: Wed, 9 Jun 2021 16:05:42 +0530 Subject: [PATCH 44/86] Update helper.js --- src/common/helper.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/helper.js b/src/common/helper.js index e35c661e..506a60f7 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -952,6 +952,7 @@ async function postEvent (topic, payload, options = {}) { 'mime-type': 'application/json', payload } + _.merge(message,options) await client.postEvent(message) await eventDispatcher.handleEvent(topic, { value: payload, options }) } From a1cf99fcb26762e3a8664989d408e85b3bc26b1b Mon Sep 17 00:00:00 2001 From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com> Date: Wed, 9 Jun 2021 16:12:33 +0530 Subject: [PATCH 45/86] Update WorkPeriodService.js --- src/services/WorkPeriodService.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index fc750bd7..cd149a36 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -235,7 +235,7 @@ async function createWorkPeriod (currentUser, workPeriod) { } } - await helper.postEvent(config.TAAS_WORK_PERIOD_CREATE_TOPIC, created.toJSON()) + await helper.postEvent(config.TAAS_WORK_PERIOD_CREATE_TOPIC, created.toJSON(),{"key":workPeriod.resourceBookingId}) return created.dataValues } @@ -289,7 +289,8 @@ async function updateWorkPeriod (currentUser, id, data) { } } - await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) + //await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) + await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), {"key":data.resourceBookingId}) return updated.dataValues } From e36a53e85f0c40e9b58e651bf1d8ce45b2216309 Mon Sep 17 00:00:00 2001 From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com> Date: Wed, 9 Jun 2021 16:30:32 +0530 Subject: [PATCH 46/86] Update helper.js --- src/common/helper.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/helper.js b/src/common/helper.js index 506a60f7..60f398c2 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -952,7 +952,9 @@ async function postEvent (topic, payload, options = {}) { 'mime-type': 'application/json', payload } - _.merge(message,options) + if (options.key) { + message.key = options.key + } await client.postEvent(message) await eventDispatcher.handleEvent(topic, { value: payload, options }) } From 7c8021465956f52274af03c842b8074f098a834b Mon Sep 17 00:00:00 2001 From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com> Date: Wed, 9 Jun 2021 16:35:41 +0530 Subject: [PATCH 47/86] Update WorkPeriodService.js --- src/services/WorkPeriodService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index cd149a36..e16ce290 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -290,7 +290,7 @@ async function updateWorkPeriod (currentUser, id, data) { } //await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) - await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), {"key":data.resourceBookingId}) + await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), {oldValue: oldValue, "key":data.resourceBookingId}) return updated.dataValues } From 7c8cf2187e87a3bdbb80caaceff8353312da18da Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 9 Jun 2021 17:02:16 +0300 Subject: [PATCH 48/86] fix: payment schedule for real payments --- src/services/PaymentSchedulerService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/PaymentSchedulerService.js b/src/services/PaymentSchedulerService.js index 27774fb5..20ebf249 100644 --- a/src/services/PaymentSchedulerService.js +++ b/src/services/PaymentSchedulerService.js @@ -2,7 +2,7 @@ const _ = require('lodash') const config = require('config') const moment = require('moment') const models = require('../models') -const { getV3MemberDetailsByHandle, getChallenge, getChallengeResource, sleep, postEvent } = require('../common/helper') +const { getMemberDetailsByHandle, getChallenge, getChallengeResource, sleep, postEvent } = require('../common/helper') const logger = require('../common/logger') const { createChallenge, addResourceToChallenge, activateChallenge, closeChallenge } = require('./PaymentService') const { ChallengeStatus, PaymentSchedulerStatus, PaymentProcessingSwitch } = require('../../app-constants') @@ -153,7 +153,7 @@ async function processStep (paymentScheduler) { paymentScheduler.step = PaymentSchedulerStatus.ACTIVATE_CHALLENGE } else if (paymentScheduler.step === PaymentSchedulerStatus.ACTIVATE_CHALLENGE) { // 4.1. get user id - const { userId } = await withRetry(getV3MemberDetailsByHandle, [paymentScheduler.userHandle], validateError, PaymentSchedulerStatus.GET_USER_ID) + const { userId } = await withRetry(getMemberDetailsByHandle, [paymentScheduler.userHandle], validateError, PaymentSchedulerStatus.GET_USER_ID) paymentScheduler.userId = userId paymentScheduler.step = PaymentSchedulerStatus.GET_USER_ID } else if (paymentScheduler.step === PaymentSchedulerStatus.GET_USER_ID) { From af6e9886fa67e165b8d7f1c718a81523d1ecdf2f Mon Sep 17 00:00:00 2001 From: xxcxy Date: Wed, 9 Jun 2021 23:12:32 +0800 Subject: [PATCH 49/86] fix #307 --- src/services/WorkPeriodPaymentService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index c6ce3982..5db9f139 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -183,7 +183,7 @@ async function createWorkPeriodPayment (currentUser, workPeriodPayment) { const successResult = await _createSingleWorkPeriodPayment(wp, createdBy) result.push(successResult) } catch (e) { - result.push(_.extend(wp, { error: { message: e.message, code: e.httpStatus } })) + result.push(_.extend(_.pick(wp, 'workPeriodId'), { error: { message: e.message, code: e.httpStatus } })) } } return result From ae782d502f2d1621876e2e099712ef6bbc1c7704 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 9 Jun 2021 23:26:28 +0300 Subject: [PATCH 50/86] job description parser --- config/default.js | 4 +- ...coder-bookings-api.postman_collection.json | 255 ++++++-- docs/stopWords.json | 574 ++++++++++++++++++ docs/swagger.yaml | 11 +- src/services/TeamService.js | 104 +++- 5 files changed, 900 insertions(+), 48 deletions(-) create mode 100644 docs/stopWords.json diff --git a/config/default.js b/config/default.js index bbb4b722..00378f72 100644 --- a/config/default.js +++ b/config/default.js @@ -173,5 +173,7 @@ module.exports = { // 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'] + INTERNAL_MEMBER_GROUPS: process.env.INTERNAL_MEMBER_GROUPS || ['20000000', '20000001', '20000003', '20000010', '20000015'], + // Topcoder skills cache time in minutes + TOPCODER_SKILLS_CACHE_TIME: process.env.TOPCODER_SKILLS_CACHE_TIME || 60 } diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 479ddceb..b37011a5 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "18310e1b-429d-49db-8555-f4a54404271f", + "_postman_id": "d413d21d-272f-454f-b26a-0d7e3bf926d9", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -21165,6 +21165,221 @@ } ] }, + { + "name": "Get Skills by Job Description", + "item": [ + { + "name": "get skills successfully", + "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}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{ \"description\": \"Description A global leading healthcare company is seeking a strong Databricks Engineer to join their development team as they build their new Databricks workspace. Development efforts will contribute to the migration of data from Hadoop to Databricks to prepare data for visualization. Candidate must be well-versed in Databricks components and best practices, be an excellent problem solver and be comfortable working in a fast-moving, rapidly changing, and dynamic environment via Agile, SCRUM, and DevOps. PREFERRED QUALIFICATIONS: 2+ years of Azure Data Stack experience: Azure Data Services using ADF, ADLS, Databricks with PySpark, Azure DevOps & Azure Key Vault. Strong knowledge of various data warehousing methodologies and data modeling concepts. Hands-on experience using Azure, Azure data lake, Azure functions & Databricks Minimum 2-3+ years of Python experience (PySpark) Design & Develop Azure native solutions for Data Platform Minimum 3+ years of experience using Big Data ecosystem (Cloudera/Hortonworks) using Oozie, Hive, Impala, and Spark Expert in SQL and performance tuning\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "getSkillsByJobDescription" + ] + } + }, + "response": [] + }, + { + "name": "get skills by 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", + "type": "text", + "value": "Bearer invalid_token" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{ \"description\": \"Description A global leading healthcare company is seeking a strong Databricks Engineer to join their development team as they build their new Databricks workspace. Development efforts will contribute to the migration of data from Hadoop to Databricks to prepare data for visualization. Candidate must be well-versed in Databricks components and best practices, be an excellent problem solver and be comfortable working in a fast-moving, rapidly changing, and dynamic environment via Agile, SCRUM, and DevOps. PREFERRED QUALIFICATIONS: 2+ years of Azure Data Stack experience: Azure Data Services using ADF, ADLS, Databricks with PySpark, Azure DevOps & Azure Key Vault. Strong knowledge of various data warehousing methodologies and data modeling concepts. Hands-on experience using Azure, Azure data lake, Azure functions & Databricks Minimum 2-3+ years of Python experience (PySpark) Design & Develop Azure native solutions for Data Platform Minimum 3+ years of experience using Big Data ecosystem (Cloudera/Hortonworks) using Oozie, Hive, Impala, and Spark Expert in SQL and performance tuning\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "getSkillsByJobDescription" + ] + } + }, + "response": [] + }, + { + "name": "get skills by invalid 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.description\\\" is not allowed to be empty\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{ \"description\": \"\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "getSkillsByJobDescription" + ] + } + }, + "response": [] + }, + { + "name": "get skills by 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.description\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "getSkillsByJobDescription" + ] + } + }, + "response": [] + } + ] + }, { "name": "GET /taas-teams", "request": { @@ -21648,44 +21863,6 @@ }, "response": [] }, - { - "name": "POST /taas-teams/getSkillsByJobDescription", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_member}}" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"description\": \"nodejs react c++ hello\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", - "host": [ - "{{URL}}" - ], - "path": [ - "taas-teams", - "getSkillsByJobDescription" - ] - } - }, - "response": [] - }, { "name": "GET /taas-teams/:id/members", "request": { diff --git a/docs/stopWords.json b/docs/stopWords.json new file mode 100644 index 00000000..dded6681 --- /dev/null +++ b/docs/stopWords.json @@ -0,0 +1,574 @@ +[ + "dr", + "dra", + "mr", + "ms", + "a", + "a's", + "able", + "about", + "above", + "according", + "accordingly", + "across", + "actually", + "after", + "afterwards", + "again", + "against", + "ain't", + "all", + "allow", + "allows", + "almost", + "alone", + "along", + "already", + "also", + "although", + "always", + "am", + "among", + "amongst", + "an", + "and", + "another", + "any", + "anybody", + "anyhow", + "anyone", + "anything", + "anyway", + "anyways", + "anywhere", + "apart", + "appear", + "appreciate", + "appropriate", + "are", + "aren't", + "around", + "as", + "aside", + "ask", + "asking", + "associated", + "at", + "available", + "away", + "awfully", + "b", + "be", + "became", + "because", + "become", + "becomes", + "becoming", + "been", + "before", + "beforehand", + "behind", + "being", + "believe", + "below", + "beside", + "besides", + "best", + "better", + "between", + "beyond", + "both", + "brief", + "but", + "by", + "c'mon", + "c's", + "came", + "can", + "can't", + "cannot", + "cant", + "cause", + "causes", + "certain", + "certainly", + "changes", + "clearly", + "co", + "come", + "comes", + "concerning", + "consequently", + "consider", + "considering", + "contain", + "containing", + "contains", + "corresponding", + "could", + "couldn't", + "course", + "currently", + "d", + "definitely", + "described", + "despite", + "did", + "didn't", + "different", + "do", + "does", + "doesn't", + "doing", + "don't", + "done", + "down", + "downwards", + "during", + "e", + "each", + "edu", + "eg", + "eight", + "either", + "else", + "elsewhere", + "enough", + "entirely", + "especially", + "et", + "etc", + "even", + "ever", + "every", + "everybody", + "everyone", + "everything", + "everywhere", + "ex", + "exactly", + "example", + "except", + "f", + "far", + "few", + "fifth", + "first", + "five", + "followed", + "following", + "follows", + "for", + "former", + "formerly", + "forth", + "four", + "from", + "further", + "furthermore", + "g", + "get", + "gets", + "getting", + "given", + "gives", + "goes", + "going", + "gone", + "got", + "gotten", + "greetings", + "h", + "had", + "hadn't", + "happens", + "hardly", + "has", + "hasn't", + "have", + "haven't", + "having", + "he", + "he's", + "hello", + "help", + "hence", + "her", + "here", + "here's", + "hereafter", + "hereby", + "herein", + "hereupon", + "hers", + "herself", + "hi", + "him", + "himself", + "his", + "hither", + "hopefully", + "how", + "howbeit", + "however", + "i", + "i'd", + "i'll", + "i'm", + "i've", + "ie", + "if", + "ignored", + "immediate", + "in", + "inasmuch", + "inc", + "indeed", + "indicate", + "indicated", + "indicates", + "inner", + "insofar", + "instead", + "into", + "inward", + "is", + "isn't", + "it", + "it'd", + "it'll", + "it's", + "its", + "itself", + "j", + "just", + "k", + "keep", + "keeps", + "kept", + "know", + "knows", + "known", + "l", + "last", + "lately", + "later", + "latter", + "latterly", + "least", + "lest", + "let", + "let's", + "like", + "liked", + "likely", + "little", + "look", + "looking", + "looks", + "ltd", + "m", + "mainly", + "many", + "may", + "maybe", + "me", + "mean", + "meanwhile", + "merely", + "might", + "more", + "moreover", + "most", + "mostly", + "much", + "must", + "my", + "myself", + "n", + "name", + "namely", + "nd", + "near", + "nearly", + "necessary", + "need", + "needs", + "neither", + "never", + "nevertheless", + "new", + "next", + "nine", + "no", + "nobody", + "non", + "none", + "noone", + "nor", + "normally", + "not", + "nothing", + "novel", + "now", + "nowhere", + "o", + "obviously", + "of", + "off", + "often", + "oh", + "ok", + "okay", + "old", + "on", + "once", + "one", + "ones", + "only", + "onto", + "or", + "other", + "others", + "otherwise", + "ought", + "our", + "ours", + "ourselves", + "out", + "outside", + "over", + "overall", + "own", + "p", + "particular", + "particularly", + "per", + "perhaps", + "placed", + "please", + "plus", + "point", + "possible", + "presumably", + "probably", + "provides", + "q", + "que", + "quite", + "qv", + "rather", + "rd", + "re", + "really", + "reasonably", + "regarding", + "regardless", + "regards", + "relatively", + "respectively", + "right", + "s", + "said", + "same", + "saw", + "say", + "saying", + "says", + "second", + "secondly", + "see", + "seeing", + "seem", + "seemed", + "seeming", + "seems", + "seen", + "self", + "selves", + "sensible", + "sent", + "serious", + "seriously", + "seven", + "several", + "shall", + "she", + "should", + "shouldn't", + "since", + "six", + "so", + "some", + "somebody", + "somehow", + "someone", + "something", + "sometime", + "sometimes", + "somewhat", + "somewhere", + "soon", + "sorry", + "specified", + "specify", + "specifying", + "still", + "strong", + "sub", + "such", + "sup", + "sure", + "t", + "t's", + "take", + "taken", + "tell", + "tends", + "th", + "than", + "thank", + "thanks", + "thanx", + "that", + "that's", + "thats", + "the", + "their", + "theirs", + "them", + "themselves", + "then", + "thence", + "there", + "there's", + "thereafter", + "thereby", + "therefore", + "therein", + "theres", + "thereupon", + "these", + "they", + "they'd", + "they'll", + "they're", + "they've", + "think", + "third", + "this", + "thorough", + "thoroughly", + "those", + "though", + "three", + "through", + "throughout", + "thru", + "thus", + "to", + "together", + "too", + "took", + "toward", + "towards", + "tried", + "tries", + "truly", + "try", + "trying", + "twice", + "two", + "u", + "un", + "under", + "unfortunately", + "unless", + "unlikely", + "until", + "unto", + "up", + "upon", + "us", + "use", + "used", + "useful", + "uses", + "using", + "usually", + "uucp", + "v", + "value", + "various", + "very", + "via", + "viz", + "vs", + "w", + "want", + "wants", + "was", + "wasn't", + "way", + "we", + "we'd", + "we'll", + "we're", + "we've", + "welcome", + "well", + "went", + "were", + "weren't", + "what", + "what's", + "whatever", + "when", + "whence", + "whenever", + "where", + "where's", + "whereafter", + "whereas", + "whereby", + "wherein", + "whereupon", + "wherever", + "whether", + "which", + "while", + "whither", + "who", + "who's", + "whoever", + "whole", + "whom", + "whose", + "why", + "will", + "willing", + "wish", + "with", + "within", + "without", + "won't", + "wonder", + "would", + "would", + "wouldn't", + "x", + "y", + "yes", + "yet", + "you", + "you'd", + "you'll", + "you're", + "you've", + "your", + "yours", + "yourself", + "yourselves", + "z", + "zero" +] \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6e2508c1..58cfec88 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2519,7 +2519,7 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - + /work-period-payments/{id}: get: tags: @@ -3269,12 +3269,6 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - "403": - description: Forbidden - content: - application/json: - schema: - $ref: "#/components/schemas/Error" "500": description: Internal Server Error content: @@ -3755,10 +3749,13 @@ components: properties: tag: type: string + example: "Java" type: type: string + example: "taas_skill" source: type: string + example: "taas-jd-parser" Job: required: diff --git a/src/services/TeamService.js b/src/services/TeamService.js index c32b2776..28ec014a 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -15,8 +15,10 @@ const ResourceBookingService = require('./ResourceBookingService') const HttpStatus = require('http-status-codes') const { Op } = require('sequelize') const models = require('../models') +const stopWords = require('../../docs/stopWords.json') const Role = models.Role const RoleSearchRequest = models.RoleSearchRequest +const topcoderSkills = {} const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { return { @@ -60,6 +62,73 @@ async function _getJobsByProjectIds (currentUser, projectIds) { return result } +/** + * Gets topcoder skills and stores their name and compiled + * regex patters according to Levenshtein distance <=1 + */ +async function _reloadCachedTopcoderSkills () { + // do not reload if cache time is not expired + if (!_.isUndefined(topcoderSkills.time)) { + const cacheTime = config.TOPCODER_SKILLS_CACHE_TIME * 60 * 1000 + if (new Date().getTime() - topcoderSkills.time < cacheTime) { + return + } + } + // collect all skills + const skills = await helper.getAllTopcoderSkills() + // set the last cached time + topcoderSkills.time = new Date().getTime() + topcoderSkills.skills = [] + // store skill names and compiled regex paterns + _.each(skills, skill => { + topcoderSkills.skills.push({ + name: skill.name, + pattern: _compileRegexPatternForSkillName(skill.name) + }) + }) +} + +/** + * Prepares the regex pattern for the given word + * according to Levenshtein distance of 1 (insertions, deletions or substitutions) + * @param {String} skillName the name of the skill + * @returns {RegExp} the compiled regex pattern + */ +function _compileRegexPatternForSkillName (skillName) { + // split the name into its chars + let chars = _.split(skillName, '') + // escape characters reserved to regex + chars = _.map(chars, _.escapeRegExp) + // Its not a good idea to apply tolerance according to + // Levenshtein distance for the words have less than 3 letters + // We expect the skill names have 1 or 2 letters to take place + // in job description as how they are exactly spelled + if (chars.length < 3) { + return new RegExp(`^(?:${_.join(chars, '')})$`, 'i') + } + + const res = [] + // include the skill name itself + res.push(_.join(chars, '')) + // include every transposition combination + // E.g. java => ajva, jvaa, jaav + for (let i = 0; i < chars.length - 1; i++) { + res.push(_.join(_.slice(chars, 0, i), '') + chars[i + 1] + chars[i] + _.join(_.slice(chars, i + 2), '')) + } + // include every insertion combination + // E.g. java => .java, j.ava, ja.va, jav.a, java. + for (let i = 0; i <= chars.length; i++) { + res.push(_.join(_.slice(chars, 0, i), '') + '.' + _.join(_.slice(chars, i), '')) + } + // include every deletion/substitution combination + // E.g. java => .?ava, j.?va, ja.?a, jav.? + for (let i = 0; i < chars.length; i++) { + res.push(_.join(_.slice(chars, 0, i), '') + '.?' + _.join(_.slice(chars, i + 1), '')) + } + // return the regex pattern + return new RegExp(`^(?:${_.join(res, '|')})$`, 'i') +} + /** * List teams * @param {Object} currentUser the user who perform this operation @@ -763,7 +832,40 @@ getRoleBySkills.schema = Joi.object() * @returns {Object} the result */ async function getSkillsByJobDescription (currentUser, data) { - return helper.getTags(data.description) + // load topcoder skills if needed. Using cached skills helps to avoid + // unnecessary api calls which is extremely time comsuming. + await _reloadCachedTopcoderSkills() + // replace markdown tags with spaces + let description = _.replace(data.description, /[`|^[\]{}~/,:-]|#{2,}|
/gi, ' ') + // replace all whitespace characters with single space + description = _.replace(description, /\s\s+/g, ' ') + // extract words from description + let words = _.split(description, ' ') + // remove stopwords from description + words = _.filter(words, word => stopWords.indexOf(word.toLowerCase()) === -1) + let foundSkills = [] + const result = [] + // try to match each word with skill names + // using pre-compiled regex pattern + _.each(words, word => { + _.each(topcoderSkills.skills, skill => { + // do not stop searching after a match in order to detect more lookalikes + if (skill.pattern.test(word)) { + foundSkills.push(skill.name) + } + }) + }) + foundSkills = _.uniq(foundSkills) + // apply desired template + _.each(foundSkills, skill => { + result.push({ + tag: skill, + type: 'taas_skill', + source: 'taas-jd-parser' + }) + }) + + return result } getSkillsByJobDescription.schema = Joi.object() From 4064e5e79db26cdd4eb0b726e377ba0ee9f9090e Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 10 Jun 2021 21:31:22 +0300 Subject: [PATCH 51/86] set RB dates to null logic --- ...coder-bookings-api.postman_collection.json | 48 ++++++- src/services/ResourceBookingService.js | 4 + test/unit/ResourceBookingService.test.js | 55 ++++++++ test/unit/common/ResourceBookingData.js | 131 +++++++++++++++++- 4 files changed, 236 insertions(+), 2 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 479ddceb..094ce95f 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "18310e1b-429d-49db-8555-f4a54404271f", + "_postman_id": "0f0620d2-acea-4e72-8d4c-6bb285d8a16a", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -13213,6 +13213,52 @@ }, "response": [] }, + { + "name": "patch resource booking set dates null", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"startDate\": null,\r\n \"endDate\": null\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/resourceBookings/{{resourceBookingId}}", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings", + "{{resourceBookingId}}" + ] + } + }, + "response": [] + }, { "name": "delete resource booking with member", "event": [ diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 7be3833e..e0fac73f 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -320,6 +320,10 @@ async function updateResourceBooking (currentUser, id, data) { const resourceBooking = await ResourceBooking.findById(id) const oldValue = resourceBooking.toJSON() + // We can't remove dates of Resource Booking once they are both set + if (!_.isNil(oldValue.startDate) && !_.isNil(oldValue.endDate) && (_.isNull(data.startDate) || _.isNull(data.endDate))) { + throw new errors.BadRequestError('You cannot remove start or end date if both are already set for Resource Booking.') + } // before updating the record, we need to check if any paid work periods tried to be deleted await _ensurePaidWorkPeriodsNotDeleted(id, oldValue, data) diff --git a/test/unit/ResourceBookingService.test.js b/test/unit/ResourceBookingService.test.js index 7b52bde2..e5052848 100644 --- a/test/unit/ResourceBookingService.test.js +++ b/test/unit/ResourceBookingService.test.js @@ -579,4 +579,59 @@ describe('resourceBooking service test', () => { expect(error.message).to.eq(data.error.message) }) }) + describe('Update resource booking dates to null', () => { + it('T36:Should not allow setting dates to null if both dates are not null', async () => { + const data = testData.T36 + const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { + return data.resourceBooking.value + }) + let error + try { + await service.partiallyUpdateResourceBooking(commonData.currentUser, data.resourceBooking.value.dataValues.id, data.resourceBooking.request) + } catch (err) { + error = err + } + expect(error.httpStatus).to.eq(data.error.httpStatus) + expect(error.message).to.eq(data.error.message) + expect(stubResourceBookingFindById.calledOnce).to.be.true + expect(stubPostEvent.notCalled).to.be.true + expect(stubCreateWorkPeriodService.callCount).to.eq(0) + expect(stubUpdateWorkPeriodService.callCount).to.eq(0) + expect(stubDeleteWorkPeriodService.callCount).to.eq(0) + }) + it('T37:Should allow setting dates to null if one of the dates is null', async () => { + const data = testData.T37 + const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { + return data.resourceBooking.value + }) + const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async () => { + return data.workPeriod.response + }) + const entity = await service.partiallyUpdateResourceBooking(commonData.currentUser, data.resourceBooking.value.dataValues.id, data.resourceBooking.request) + expect(entity).to.deep.eql(data.resourceBooking.response.toJSON()) + expect(stubResourceBookingFindById.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true + expect(stubWorkPeriodFindAll.called).to.be.true + expect(stubCreateWorkPeriodService.callCount).to.eq(0) + expect(stubUpdateWorkPeriodService.callCount).to.eq(0) + expect(stubDeleteWorkPeriodService.callCount).to.eq(0) + }) + it('T38:Should allow setting dates to null if both dates are null', async () => { + const data = testData.T38 + const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { + return data.resourceBooking.value + }) + const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async () => { + return data.workPeriod.response + }) + const entity = await service.partiallyUpdateResourceBooking(commonData.currentUser, data.resourceBooking.value.dataValues.id, data.resourceBooking.request) + expect(entity).to.deep.eql(data.resourceBooking.response.toJSON()) + expect(stubResourceBookingFindById.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true + expect(stubWorkPeriodFindAll.called).to.be.true + expect(stubCreateWorkPeriodService.callCount).to.eq(0) + expect(stubUpdateWorkPeriodService.callCount).to.eq(0) + expect(stubDeleteWorkPeriodService.callCount).to.eq(0) + }) + }) }) diff --git a/test/unit/common/ResourceBookingData.js b/test/unit/common/ResourceBookingData.js index 78732e91..18d2ece5 100644 --- a/test/unit/common/ResourceBookingData.js +++ b/test/unit/common/ResourceBookingData.js @@ -1351,6 +1351,132 @@ const T35 = { message: 'Can not filter or sort by some field which is not included in fields' } } +const T36 = { + resourceBooking: { + value: { + dataValues: { + id: '520bb632-a02a-415e-9857-93b2ecbf7d60', + projectId: 21, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '6093e58c-683d-4022-8482-5515e8345016', + startDate: '2021-04-05', + endDate: '2021-04-17', + memberRate: 13.23, + customerRate: 13, + rateType: 'hourly', + createdAt: '2020-10-09T04:24:01.048Z', + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + status: 'sourcing', + billingAccountId: 68800079 + } + }, + request: { + startDate: '2021-04-05', + endDate: null + } + }, + error: { + httpStatus: 400, + message: 'You cannot remove start or end date if both are already set for Resource Booking.' + } +} +T36.resourceBooking.value.toJSON = () => T36.resourceBooking.value.dataValues +const T37 = { + resourceBooking: { + value: { + dataValues: { + id: '520bb632-a02a-415e-9857-93b2ecbf7d60', + projectId: 21, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '6093e58c-683d-4022-8482-5515e8345016', + startDate: null, + endDate: '2021-04-17', + memberRate: 13.23, + customerRate: 13, + rateType: 'hourly', + createdAt: '2020-10-09T04:24:01.048Z', + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + status: 'sourcing', + billingAccountId: 68800079 + } + }, + request: { + startDate: null, + endDate: null + }, + response: { + dataValues: { + id: '520bb632-a02a-415e-9857-93b2ecbf7d60', + projectId: 21, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '6093e58c-683d-4022-8482-5515e8345016', + startDate: null, + endDate: null, + memberRate: 13.23, + customerRate: 13, + rateType: 'hourly', + createdAt: '2020-10-09T04:24:01.048Z', + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + status: 'sourcing', + billingAccountId: 68800079 + } + } + }, + workPeriod: { + response: [] + } +} +T37.resourceBooking.value.toJSON = () => T37.resourceBooking.value.dataValues +T37.resourceBooking.value.update = () => T37.resourceBooking.response +T37.resourceBooking.response.toJSON = () => T37.resourceBooking.response.dataValues +const T38 = { + resourceBooking: { + value: { + dataValues: { + id: '520bb632-a02a-415e-9857-93b2ecbf7d60', + projectId: 21, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '6093e58c-683d-4022-8482-5515e8345016', + startDate: null, + endDate: null, + memberRate: 13.23, + customerRate: 13, + rateType: 'hourly', + createdAt: '2020-10-09T04:24:01.048Z', + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + status: 'sourcing', + billingAccountId: 68800079 + } + }, + request: { + startDate: null, + endDate: null + }, + response: { + dataValues: { + id: '520bb632-a02a-415e-9857-93b2ecbf7d60', + projectId: 21, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '6093e58c-683d-4022-8482-5515e8345016', + startDate: null, + endDate: null, + memberRate: 13.23, + customerRate: 13, + rateType: 'hourly', + createdAt: '2020-10-09T04:24:01.048Z', + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + status: 'sourcing', + billingAccountId: 68800079 + } + } + }, + workPeriod: { + response: [] + } +} +T38.resourceBooking.value.toJSON = () => T38.resourceBooking.value.dataValues +T38.resourceBooking.value.update = () => T38.resourceBooking.response +T38.resourceBooking.response.toJSON = () => T38.resourceBooking.response.dataValues module.exports = { T01, T02, @@ -1386,5 +1512,8 @@ module.exports = { T32, T33, T34, - T35 + T35, + T36, + T37, + T38 } From 7f168a43b2f780971db3d4844c44559d0d932e1f Mon Sep 17 00:00:00 2001 From: xxcxy Date: Fri, 11 Jun 2021 12:02:43 +0800 Subject: [PATCH 52/86] fix #332 --- docs/swagger.yaml | 20 ++++++++++---- src/services/ResourceBookingService.js | 38 ++++++++++++++++++++------ src/services/WorkPeriodService.js | 30 ++++++++++++++++---- 3 files changed, 68 insertions(+), 20 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6e2508c1..9c1c99b8 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1505,9 +1505,13 @@ paths: name: workPeriods.paymentStatus required: false schema: - type: string - enum: ["pending", "partially-completed", "completed", "cancelled"] - description: The payment status. + oneOf: + - type: array + items: + type: string + enum: ["pending", "partially-completed", "completed", "cancelled"] + - type: string + description: comma separated payment status. - in: query name: workPeriods.startDate required: false @@ -1956,9 +1960,13 @@ paths: name: paymentStatus required: false schema: - type: string - enum: ["pending", "partially-completed", "completed", "cancelled"] - description: The payment status. + oneOf: + - type: array + items: + type: string + enum: ["pending", "partially-completed", "completed", "cancelled"] + - type: string + description: comma separated payment status. - in: query name: startDate required: false diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 7be3833e..a23c336e 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -454,6 +454,11 @@ async function searchResourceBookings (currentUser, criteria, options = { return return projectId }) } + // `criteria[workPeriods.paymentStatus]` could be array of paymentStatus, or comma separated string of paymentStatus + // in case it's comma separated string of paymentStatus we have to convert it to an array of paymentStatus + if ((typeof criteria['workPeriods.paymentStatus']) === 'string') { + criteria['workPeriods.paymentStatus'] = criteria['workPeriods.paymentStatus'].trim().split(',').map(ps => Joi.attempt({ paymentStatus: ps.trim() }, Joi.object().keys({ paymentStatus: Joi.paymentStatus() })).paymentStatus) + } const page = criteria.page let perPage if (options.returnAll) { @@ -535,13 +540,21 @@ async function searchResourceBookings (currentUser, criteria, options = { return if (!_.isEmpty(workPeriodFilters)) { const workPeriodsMust = [] _.each(workPeriodFilters, (value, key) => { - workPeriodsMust.push({ - term: { - [key]: { - value + if (key === 'workPeriods.paymentStatus') { + workPeriodsMust.push({ + terms: { + [key]: value } - } - }) + }) + } else { + workPeriodsMust.push({ + term: { + [key]: { + value + } + } + }) + } }) esQuery.body.query.bool.must.push({ @@ -560,7 +573,13 @@ async function searchResourceBookings (currentUser, criteria, options = { return _.each(_.omit(workPeriodFilters, 'workPeriods.userHandle'), (value, key) => { key = key.split('.')[1] _.each(resourceBookings, r => { - r.workPeriods = _.filter(r.workPeriods, { [key]: value }) + r.workPeriods = _.filter(r.workPeriods, wp => { + if (key === 'paymentStatus') { + return _.includes(value, wp[key]) + } else { + return wp[key] === value + } + }) }) }) @@ -664,7 +683,10 @@ searchResourceBookings.schema = Joi.object().keys({ Joi.string(), Joi.array().items(Joi.number().integer()) ), - 'workPeriods.paymentStatus': Joi.paymentStatus(), + 'workPeriods.paymentStatus': Joi.alternatives( + Joi.string(), + Joi.array().items(Joi.paymentStatus()) + ), 'workPeriods.startDate': Joi.date().format('YYYY-MM-DD'), 'workPeriods.endDate': Joi.date().format('YYYY-MM-DD'), 'workPeriods.userHandle': Joi.string() diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index e16ce290..64f9a568 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -235,7 +235,7 @@ async function createWorkPeriod (currentUser, workPeriod) { } } - await helper.postEvent(config.TAAS_WORK_PERIOD_CREATE_TOPIC, created.toJSON(),{"key":workPeriod.resourceBookingId}) + await helper.postEvent(config.TAAS_WORK_PERIOD_CREATE_TOPIC, created.toJSON(), { key: workPeriod.resourceBookingId }) return created.dataValues } @@ -289,8 +289,8 @@ async function updateWorkPeriod (currentUser, id, data) { } } - //await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) - await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), {oldValue: oldValue, "key":data.resourceBookingId}) + // await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) + await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue, key: data.resourceBookingId }) return updated.dataValues } @@ -401,6 +401,11 @@ async function searchWorkPeriods (currentUser, criteria, options = { returnAll: return resourceBookingId }) } + // `criteria.paymentStatus` could be array of paymentStatus, or comma separated string of paymentStatus + // in case it's comma separated string of paymentStatus we have to convert it to an array of paymentStatus + if ((typeof criteria.paymentStatus) === 'string') { + criteria.paymentStatus = criteria.paymentStatus.trim().split(',').map(ps => Joi.attempt({ paymentStatus: ps.trim() }, Joi.object().keys({ paymentStatus: Joi.paymentStatus() })).paymentStatus) + } const page = criteria.page const perPage = criteria.perPage if (!criteria.sortBy) { @@ -436,7 +441,7 @@ async function searchWorkPeriods (currentUser, criteria, options = { returnAll: criteria.endDate = moment(criteria.endDate).format('YYYY-MM-DD') } // Apply filters - _.each(_.pick(criteria, ['resourceBookingId', 'userHandle', 'projectId', 'startDate', 'endDate', 'paymentStatus']), (value, key) => { + _.each(_.pick(criteria, ['resourceBookingId', 'userHandle', 'projectId', 'startDate', 'endDate']), (value, key) => { esQuery.body.query.nested.query.bool.must.push({ term: { [`workPeriods.${key}`]: { @@ -445,6 +450,13 @@ async function searchWorkPeriods (currentUser, criteria, options = { returnAll: } }) }) + if (criteria.paymentStatus) { + esQuery.body.query.nested.query.bool.must.push({ + terms: { + 'workPeriods.paymentStatus': criteria.paymentStatus + } + }) + } // if criteria contains resourceBookingIds, filter resourceBookingId with this value if (criteria.resourceBookingIds) { esQuery.body.query.nested.query.bool.filter = [{ @@ -459,9 +471,12 @@ async function searchWorkPeriods (currentUser, criteria, options = { returnAll: let workPeriods = _.reduce(body.hits.hits, (acc, resourceBooking) => _.concat(acc, resourceBooking._source.workPeriods), []) // ESClient will return ResourceBookings with it's all nested WorkPeriods // We re-apply WorkPeriod filters - _.each(_.pick(criteria, ['startDate', 'endDate', 'paymentStatus']), (value, key) => { + _.each(_.pick(criteria, ['startDate', 'endDate']), (value, key) => { workPeriods = _.filter(workPeriods, { [key]: value }) }) + if (criteria.paymentStatus) { + workPeriods = _.filter(workPeriods, wp => _.includes(criteria.paymentStatus, wp.paymentStatus)) + } workPeriods = _.sortBy(workPeriods, [criteria.sortBy]) if (criteria.sortOrder === 'desc') { workPeriods = _.reverse(workPeriods) @@ -522,7 +537,10 @@ searchWorkPeriods.schema = Joi.object().keys({ perPage: Joi.number().integer().min(1).max(10000).default(20), sortBy: Joi.string().valid('id', 'resourceBookingId', 'userHandle', 'projectId', 'startDate', 'endDate', 'daysWorked', 'customerRate', 'memberRate', 'paymentStatus'), sortOrder: Joi.string().valid('desc', 'asc'), - paymentStatus: Joi.paymentStatus(), + paymentStatus: Joi.alternatives( + Joi.string(), + Joi.array().items(Joi.paymentStatus()) + ), startDate: Joi.date().format('YYYY-MM-DD'), endDate: Joi.date().format('YYYY-MM-DD'), userHandle: Joi.string(), From e2fc2dd3f1fae9b27a377694c78565c24225722e Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Fri, 11 Jun 2021 10:08:01 +0530 Subject: [PATCH 53/86] fix: Added allow empty validation for string fields in Job and Jobs Canidate services --- src/services/JobCandidateService.js | 4 ++-- src/services/JobService.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index cc059c0c..72c1e7c4 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -203,8 +203,8 @@ fullyUpdateJobCandidate.schema = Joi.object().keys({ userId: Joi.string().uuid().required(), status: Joi.jobCandidateStatus().default('open'), externalId: Joi.string().allow(null).default(null), - resume: Joi.string().uri().allow(null).default(null), - remark: Joi.string().allow(null).default(null) + resume: Joi.string().stringAllowEmpty().uri().stringAllowEmpty().allow(null).default(null), + remark: Joi.string().stringAllowEmpty().allow(null).default(null) }).required() }).required() diff --git a/src/services/JobService.js b/src/services/JobService.js index 1947fb9e..4ce0fdc7 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -206,9 +206,9 @@ createJob.schema = Joi.object().keys({ minSalary: Joi.number().integer().allow(null), maxSalary: Joi.number().integer().allow(null), hoursPerWeek: Joi.number().integer().allow(null), - jobLocation: Joi.string().allow(null), - jobTimezone: Joi.string().allow(null), - currency: Joi.string().allow(null), + jobLocation: Joi.string().stringAllowEmpty().allow(null), + jobTimezone: Joi.string().stringAllowEmpty().allow(null), + currency: Joi.string().stringAllowEmpty().allow(null), roleIds: Joi.array().items(Joi.string().uuid().required()) }).required() }).required() From 75b9bafb11b65a1fca66b3ae07966fba1c0e9d4c Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Fri, 11 Jun 2021 10:10:33 +0530 Subject: [PATCH 54/86] ci: deploy on dev env --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index e6268124..d634f7e6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -68,6 +68,7 @@ workflows: branches: only: - dev + - change-validatations-in-job-jc # Production builds are exectuted only on tagged commits to the # master branch. From 5d23c818637fecfbb5bdf675f632c8a19f49c6a7 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 11 Jun 2021 09:54:07 +0300 Subject: [PATCH 55/86] fix: migration script for scheduler --- ...-payment-scheduler-table-add-status-details-to-payment.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js b/migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js index 5eb2232d..3931f4bb 100644 --- a/migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js +++ b/migrations/2021-05-29-create-payment-scheduler-table-add-status-details-to-payment.js @@ -10,7 +10,7 @@ const { PaymentSchedulerStatus } = require('../app-constants') module.exports = { up: async (queryInterface, Sequelize) => { const transaction = await queryInterface.sequelize.transaction() - try { + try { await queryInterface.createTable('payment_schedulers', { id: { type: Sequelize.UUID, @@ -75,7 +75,6 @@ module.exports = { await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'challenge_id', { type: Sequelize.UUID }, { transaction }) - await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'scheduled'`) await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'in-progress'`) await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'failed'`) await transaction.commit() @@ -101,7 +100,7 @@ module.exports = { { transaction }) await queryInterface.removeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'status_details', { transaction }) - await queryInterface.sequelize.query(`DELETE FROM pg_enum WHERE enumlabel in ('scheduled', 'in-progress', 'failed') AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'enum_work_period_payments_status')`, + await queryInterface.sequelize.query(`DELETE FROM pg_enum WHERE enumlabel in ('in-progress', 'failed') AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'enum_work_period_payments_status')`, { transaction }) await transaction.commit() } catch (err) { From ff91b389d6b0ffaa2784d3e1f6d470d086fc7b06 Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Fri, 11 Jun 2021 13:12:53 +0530 Subject: [PATCH 56/86] fix: added annunal, another rate type --- src/bootstrap.js | 2 +- src/services/JobCandidateService.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bootstrap.js b/src/bootstrap.js index 259c4a2c..e8f7ad2b 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -10,7 +10,7 @@ const allowedXAITemplate = _.keys(Interviews.XaiTemplate) Joi.page = () => Joi.number().integer().min(1).default(1) Joi.perPage = () => Joi.number().integer().min(1).default(20) -Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') +Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly','annual') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index 72c1e7c4..5d52792a 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -203,7 +203,7 @@ fullyUpdateJobCandidate.schema = Joi.object().keys({ userId: Joi.string().uuid().required(), status: Joi.jobCandidateStatus().default('open'), externalId: Joi.string().allow(null).default(null), - resume: Joi.string().stringAllowEmpty().uri().stringAllowEmpty().allow(null).default(null), + resume: Joi.string().stringAllowEmpty().uri().allow(null).default(null), remark: Joi.string().stringAllowEmpty().allow(null).default(null) }).required() }).required() From 09368f116d41e62b2b71a33ca74ccab9c0bb1ae9 Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Fri, 11 Jun 2021 13:55:16 +0530 Subject: [PATCH 57/86] fix: allow empty string --- src/services/JobService.js | 54 ++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/src/services/JobService.js b/src/services/JobService.js index 4ce0fdc7..5c46a9fc 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -187,31 +187,35 @@ async function createJob (currentUser, job) { return created.toJSON() } -createJob.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - job: Joi.object().keys({ - status: Joi.jobStatus().default('sourcing'), - projectId: Joi.number().integer().required(), - externalId: Joi.string().allow(null), - description: Joi.stringAllowEmpty().allow(null), - title: Joi.title().required(), - startDate: Joi.date().allow(null), - duration: Joi.number().integer().min(1).allow(null), - numPositions: Joi.number().integer().min(1).required(), - resourceType: Joi.stringAllowEmpty().allow(null), - rateType: Joi.rateType().allow(null), - workload: Joi.workload().allow(null), - skills: Joi.array().items(Joi.string().uuid()).required(), - isApplicationPageActive: Joi.boolean(), - minSalary: Joi.number().integer().allow(null), - maxSalary: Joi.number().integer().allow(null), - hoursPerWeek: Joi.number().integer().allow(null), - jobLocation: Joi.string().stringAllowEmpty().allow(null), - jobTimezone: Joi.string().stringAllowEmpty().allow(null), - currency: Joi.string().stringAllowEmpty().allow(null), - roleIds: Joi.array().items(Joi.string().uuid().required()) - }).required() -}).required() +createJob.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + job: Joi.object() + .keys({ + status: Joi.jobStatus().default("sourcing"), + projectId: Joi.number().integer().required(), + externalId: Joi.string().allow(null), + description: Joi.stringAllowEmpty().allow(null), + title: Joi.title().required(), + startDate: Joi.date().allow(null), + duration: Joi.number().integer().min(1).allow(null), + numPositions: Joi.number().integer().min(1).required(), + resourceType: Joi.stringAllowEmpty().allow(null), + rateType: Joi.rateType().allow(null), + workload: Joi.workload().allow(null), + skills: Joi.array().items(Joi.string().uuid()).required(), + isApplicationPageActive: Joi.boolean(), + minSalary: Joi.number().integer().allow(null), + maxSalary: Joi.number().integer().allow(null), + hoursPerWeek: Joi.number().integer().allow(null), + jobLocation: Joi.string().allow(null).stringAllowEmpty(), + jobTimezone: Joi.string().allow(null).stringAllowEmpty(), + currency: Joi.string().allow(null).stringAllowEmpty(), + roleIds: Joi.array().items(Joi.string().uuid().required()), + }) + .required(), + }) + .required(); /** * Update job. Normal user can only update the job he/she created. From d898555f0810d1e1fa0e444d1136c10ae660addd Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Fri, 11 Jun 2021 13:57:38 +0530 Subject: [PATCH 58/86] fix: allow empty --- src/services/JobService.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/JobService.js b/src/services/JobService.js index 5c46a9fc..3348a295 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -208,9 +208,9 @@ createJob.schema = Joi.object() minSalary: Joi.number().integer().allow(null), maxSalary: Joi.number().integer().allow(null), hoursPerWeek: Joi.number().integer().allow(null), - jobLocation: Joi.string().allow(null).stringAllowEmpty(), - jobTimezone: Joi.string().allow(null).stringAllowEmpty(), - currency: Joi.string().allow(null).stringAllowEmpty(), + jobLocation: Joi.string().allow(null).allow(''), + jobTimezone: Joi.string().allow(null).allow(''), + currency: Joi.string().allow(null).allow(''), roleIds: Joi.array().items(Joi.string().uuid().required()), }) .required(), From dcb0a139dce213553fce4d6bb69910e669ecfb9f Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Fri, 11 Jun 2021 13:58:56 +0530 Subject: [PATCH 59/86] fix: empty string fix --- src/services/JobCandidateService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index 5d52792a..9a152c49 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -203,8 +203,8 @@ fullyUpdateJobCandidate.schema = Joi.object().keys({ userId: Joi.string().uuid().required(), status: Joi.jobCandidateStatus().default('open'), externalId: Joi.string().allow(null).default(null), - resume: Joi.string().stringAllowEmpty().uri().allow(null).default(null), - remark: Joi.string().stringAllowEmpty().allow(null).default(null) + resume: Joi.string().uri().allow('').allow(null).default(null), + remark: Joi.string().allow('').allow(null).default(null) }).required() }).required() From bf32afb221ecf1e269dd6108ad0aba96197a9c62 Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Fri, 11 Jun 2021 14:06:37 +0530 Subject: [PATCH 60/86] fix: updated swagger --- docs/swagger.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6e2508c1..e224cb16 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -174,7 +174,7 @@ paths: required: false schema: type: string - enum: ["hourly", "daily", "weekly", "monthly"] + enum: ["hourly", "daily", "weekly", "monthly","annual"] description: The rate type. - in: query name: status From da725280342fbe455947f44356941675e3eea8c7 Mon Sep 17 00:00:00 2001 From: xxcxy Date: Fri, 11 Jun 2021 16:41:01 +0800 Subject: [PATCH 61/86] Include addition param key to Postevent method #329 --- src/common/helper.js | 4 ++-- src/services/WorkPeriodPaymentService.js | 4 ++-- src/services/WorkPeriodService.js | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 60f398c2..3f0cb27a 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -953,8 +953,8 @@ async function postEvent (topic, payload, options = {}) { payload } if (options.key) { - message.key = options.key - } + message.key = options.key + } await client.postEvent(message) await eventDispatcher.handleEvent(topic, { value: payload, options }) } diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index c6ce3982..1040b056 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -96,7 +96,7 @@ async function _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking (w } } - await helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC, created.toJSON()) + await helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC, created.toJSON(), { key: `workPeriodPayment.billingAccountId:${workPeriodPayment.billingAccountId}` }) return created.dataValues } @@ -234,7 +234,7 @@ async function updateWorkPeriodPayment (currentUser, id, data) { } } - await helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) + await helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue, key: `workPeriodPayment.billingAccountId:${updated.billingAccountId}` }) return updated.dataValues } diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index e16ce290..4070d88d 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -235,7 +235,7 @@ async function createWorkPeriod (currentUser, workPeriod) { } } - await helper.postEvent(config.TAAS_WORK_PERIOD_CREATE_TOPIC, created.toJSON(),{"key":workPeriod.resourceBookingId}) + await helper.postEvent(config.TAAS_WORK_PERIOD_CREATE_TOPIC, created.toJSON(), { key: `resourceBooking.id:${workPeriod.resourceBookingId}` }) return created.dataValues } @@ -289,8 +289,8 @@ async function updateWorkPeriod (currentUser, id, data) { } } - //await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) - await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), {oldValue: oldValue, "key":data.resourceBookingId}) + // await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) + await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue, key: `resourceBooking.id:${data.resourceBookingId}` }) return updated.dataValues } @@ -364,9 +364,9 @@ async function deleteWorkPeriod (currentUser, id) { workPeriodId: id } }) - await Promise.all(workPeriod.payments.map(({ id }) => helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_DELETE_TOPIC, { id }))) + await Promise.all(workPeriod.payments.map(({ id, billingAccountId }) => helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_DELETE_TOPIC, { id }, { key: `workPeriodPayment.billingAccountId:${billingAccountId}` }))) await workPeriod.destroy() - await helper.postEvent(config.TAAS_WORK_PERIOD_DELETE_TOPIC, { id }) + await helper.postEvent(config.TAAS_WORK_PERIOD_DELETE_TOPIC, { id }, { key: `resourceBooking.id:${workPeriod.resourceBookingId}` }) } deleteWorkPeriod.schema = Joi.object().keys({ From e4a02017371770dfe8c7bba7ededb16ecb57ef0d Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 11 Jun 2021 12:31:28 +0300 Subject: [PATCH 62/86] docs: improve README --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index eddfead3..287bf7cb 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,19 @@ To be able to change and test `taas-es-processor` locally you can follow the nex 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` + - Create `.env` file with the next environment variables. Values for **Auth0 config** should be shared with you on the forum.
+ + ```bash + # Auth0 config + AUTH0_URL= + AUTH0_AUDIENCE= + AUTH0_CLIENT_ID= + AUTH0_CLIENT_SECRET= + ``` + + - Values from this file would be automatically used by many `npm` commands. + - ⚠️ Never commit this file or its copy to the repository! + - `npm run start` ## NPM Commands From e6e5348b24c296adc43339d676175af4274b3de4 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 11 Jun 2021 13:45:16 +0300 Subject: [PATCH 63/86] fix: lint --- src/bootstrap.js | 2 +- src/services/JobService.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/bootstrap.js b/src/bootstrap.js index 5ca2c020..aebac2c2 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -11,7 +11,7 @@ const allowedXAITemplate = _.keys(Interviews.XaiTemplate) Joi.page = () => Joi.number().integer().min(1).default(1) Joi.perPage = () => Joi.number().integer().min(1).default(20) -Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly','annual') +Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly', 'annual') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') diff --git a/src/services/JobService.js b/src/services/JobService.js index 3348a295..7291070b 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -192,7 +192,7 @@ createJob.schema = Joi.object() currentUser: Joi.object().required(), job: Joi.object() .keys({ - status: Joi.jobStatus().default("sourcing"), + status: Joi.jobStatus().default('sourcing'), projectId: Joi.number().integer().required(), externalId: Joi.string().allow(null), description: Joi.stringAllowEmpty().allow(null), @@ -211,11 +211,11 @@ createJob.schema = Joi.object() jobLocation: Joi.string().allow(null).allow(''), jobTimezone: Joi.string().allow(null).allow(''), currency: Joi.string().allow(null).allow(''), - roleIds: Joi.array().items(Joi.string().uuid().required()), + roleIds: Joi.array().items(Joi.string().uuid().required()) }) - .required(), + .required() }) - .required(); + .required() /** * Update job. Normal user can only update the job he/she created. From 6ba5f160a04bb7a3b2750e69a872a411378a866f Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 11 Jun 2021 13:50:05 +0300 Subject: [PATCH 64/86] docs: improve Swagger --- docs/swagger.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 8c36fdf2..cab269c9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1511,6 +1511,7 @@ paths: type: string enum: ["pending", "partially-completed", "completed", "cancelled"] - type: string + enum: ["pending", "partially-completed", "completed", "cancelled"] description: comma separated payment status. - in: query name: workPeriods.startDate @@ -1966,6 +1967,7 @@ paths: type: string enum: ["pending", "partially-completed", "completed", "cancelled"] - type: string + enum: ["pending", "partially-completed", "completed", "cancelled"] description: comma separated payment status. - in: query name: startDate From cf7aeacc2a66ce3c0af6733655441873c5efa776 Mon Sep 17 00:00:00 2001 From: xxcxy Date: Fri, 11 Jun 2021 23:15:32 +0800 Subject: [PATCH 65/86] fix #337 --- src/common/helper.js | 1 + src/services/InterviewService.js | 3 ++- src/services/JobCandidateService.js | 3 ++- src/services/JobService.js | 3 ++- src/services/WorkPeriodPaymentService.js | 3 ++- src/services/WorkPeriodService.js | 5 +++-- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index f4a1aac2..45e4afe4 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -816,6 +816,7 @@ function setResHeaders (req, res, result) { res.set('X-Per-Page', result.perPage) res.set('X-Total', result.total) res.set('X-Total-Pages', totalPages) + res.set('X-Data-Source', result.fromDb ? 'database' : 'elasticsearch') // set Link header if (totalPages > 0) { let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink( diff --git a/src/services/InterviewService.js b/src/services/InterviewService.js index a69a788c..3ddb1a3d 100644 --- a/src/services/InterviewService.js +++ b/src/services/InterviewService.js @@ -539,9 +539,10 @@ async function searchInterviews (currentUser, jobCandidateId, criteria) { limit: perPage, order: [[criteria.sortBy, criteria.sortOrder]] }) + const total = await Interview.count({ where: filter }) return { fromDb: true, - total: interviews.length, + total, page, perPage, result: _.map(interviews, interview => interview.dataValues) diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index 9a152c49..1f283512 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -316,9 +316,10 @@ async function searchJobCandidates (currentUser, criteria) { limit: perPage, order: [[criteria.sortBy, criteria.sortOrder]] }) + const total = await JobCandidate.count({ where: filter }) return { fromDb: true, - total: jobCandidates.length, + total, page, perPage, result: _.map(jobCandidates, jobCandidate => _.omit(jobCandidate.dataValues, omitList)) diff --git a/src/services/JobService.js b/src/services/JobService.js index 7291070b..06e3b67c 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -532,9 +532,10 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } required: false }] }) + const total = await Job.count({ where: filter }) return { fromDb: true, - total: jobs.length, + total, page, perPage, result: _.map(jobs, job => job.dataValues) diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index 80299628..38582f41 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -381,9 +381,10 @@ async function searchWorkPeriodPayments (currentUser, criteria, options = { retu limit: perPage, order: [[criteria.sortBy, criteria.sortOrder]] }) + const total = await WorkPeriodPayment.count({ where: filter }) return { fromDb: true, - total: workPeriodPayments.length, + total, page, perPage, result: workPeriodPayments diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index 9e555b90..71108e46 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -519,10 +519,11 @@ async function searchWorkPeriods (currentUser, criteria, options = { returnAll: required: false }] } - const workPeriods = await WorkPeriod.findAll(queryCriteria) + const workPeriods = await WorkPeriod.findAndCountAll(queryCriteria) + const total = await WorkPeriod.count({ where: filter }) return { fromDb: true, - total: workPeriods.length, + total, page, perPage, result: workPeriods From 30c627a3e88fbea827b7c4964d4e0aad15f70dfe Mon Sep 17 00:00:00 2001 From: eisbilir Date: Sat, 12 Jun 2021 00:16:24 +0300 Subject: [PATCH 66/86] calculate daysWorked --- src/common/helper.js | 4 +- .../ResourceBookingEventHandler.js | 83 +- src/services/ResourceBookingService.js | 30 +- src/services/WorkPeriodService.js | 13 +- test/unit/ResourceBookingService.test.js | 106 ++- test/unit/common/ResourceBookingData.js | 714 ++++++++++-------- 6 files changed, 582 insertions(+), 368 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 60f398c2..3f0cb27a 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -953,8 +953,8 @@ async function postEvent (topic, payload, options = {}) { payload } if (options.key) { - message.key = options.key - } + message.key = options.key + } await client.postEvent(message) await eventDispatcher.handleEvent(topic, { value: payload, options }) } diff --git a/src/eventHandlers/ResourceBookingEventHandler.js b/src/eventHandlers/ResourceBookingEventHandler.js index ef5825fc..c82c4e35 100644 --- a/src/eventHandlers/ResourceBookingEventHandler.js +++ b/src/eventHandlers/ResourceBookingEventHandler.js @@ -176,15 +176,45 @@ async function updateWorkPeriods (payload) { const workPeriodsToRemove = _.differenceBy(workPeriods, newWorkPeriods, 'startDate') // find which workperiods should be created const workPeriodsToAdd = _.differenceBy(newWorkPeriods, workPeriods, 'startDate') - // find which workperiods' daysWorked propery should be updated - let workPeriodsToUpdate = _.intersectionBy(newWorkPeriods, workPeriods, 'startDate') - // find which workperiods' daysWorked property is preset and exceeds the possible maximum - workPeriodsToUpdate = _.differenceWith(workPeriodsToUpdate, workPeriods, (a, b) => b.startDate === a.startDate && _.defaultTo(b.daysWorked, a.daysWorked) <= a.daysWorked) - // include id - workPeriodsToUpdate = _.map(workPeriodsToUpdate, wpu => { - wpu.id = _.filter(workPeriods, ['startDate', wpu.startDate])[0].id - return wpu - }) + // find which workperiods' daysWorked property should be evaluated for changes + const IntersectedWorkPeriods = _.intersectionBy(newWorkPeriods, workPeriods, 'startDate') + let workPeriodsToUpdate = [] + if (IntersectedWorkPeriods.length > 0) { + // We only need check for first and last ones of intersected workPeriods + // The ones at the middle won't be updated and their daysWorked value will stay the same + if (payload.options.oldValue.startDate !== payload.value.startDate) { + const firstWeek = _.minBy(IntersectedWorkPeriods, 'startDate') + const originalFirstWeek = _.find(workPeriods, ['startDate', firstWeek.startDate]) + // recalculate daysWorked for the first week of existent workPeriods + if (firstWeek.startDate === _.minBy(workPeriods, 'startDate').startDate) { + workPeriodsToUpdate.push(_.assign(firstWeek, { id: originalFirstWeek.id })) + // if first of intersected workPeriods is not the first one of existent workPeriods + // we only check if it's daysWorked exceeds the possible maximum + } else if (originalFirstWeek.daysWorked > firstWeek.daysWorked) { + workPeriodsToUpdate.push(_.assign(firstWeek, { id: originalFirstWeek.id })) + } + } + if (payload.options.oldValue.endDate !== payload.value.endDate) { + const lastWeek = _.maxBy(IntersectedWorkPeriods, 'startDate') + const originalLastWeek = _.find(workPeriods, ['startDate', lastWeek.startDate]) + // recalculate daysWorked for the last week of existent workPeriods + if (lastWeek.startDate === _.maxBy(workPeriods, 'startDate').startDate) { + workPeriodsToUpdate.push(_.assign(lastWeek, { id: originalLastWeek.id })) + // if last of intersected workPeriods is not the last one of existent workPeriods + // we only check if it's daysWorked exceeds the possible maximum + } else if (originalLastWeek.daysWorked > lastWeek.daysWorked) { + workPeriodsToUpdate.push(_.assign(lastWeek, { id: originalLastWeek.id })) + } + } + } + // if intersected WP count is 1, this can result to duplicated WorkPeriods. + // We should choose the one with higher daysWorked because, it's more likely + // the WP we applied "first/last one of existent WPs" logic above. + if (workPeriodsToUpdate.length === 2) { + if (workPeriodsToUpdate[0].startDate === workPeriodsToUpdate[1].startDate) { + workPeriodsToUpdate = [_.maxBy(workPeriodsToUpdate, 'daysWorked')] + } + } if (workPeriodsToRemove.length === 0 && workPeriodsToAdd.length === 0 && workPeriodsToUpdate.length === 0) { logger.debug({ component: 'ResourceBookingEventHandler', @@ -256,14 +286,16 @@ async function deleteWorkPeriods (payload) { * @returns {undefined} */ async function _createWorkPeriods (periods, resourceBookingId) { - await Promise.all(_.map(periods, async period => await WorkPeriodService.createWorkPeriod(helper.getAuditM2Muser(), - { - resourceBookingId: resourceBookingId, - startDate: period.startDate, - endDate: period.endDate, - daysWorked: null, - paymentStatus: 'pending' - }))) + for (const period of periods) { + await WorkPeriodService.createWorkPeriod(helper.getAuditM2Muser(), + { + resourceBookingId: resourceBookingId, + startDate: period.startDate, + endDate: period.endDate, + daysWorked: period.daysWorked, + paymentStatus: 'pending' + }) + } } /** @@ -272,11 +304,13 @@ async function _createWorkPeriods (periods, resourceBookingId) { * @returns {undefined} */ async function _updateWorkPeriods (periods) { - await Promise.all(_.map(periods, async period => await WorkPeriodService.partiallyUpdateWorkPeriod(helper.getAuditM2Muser(), - period.id, - { - daysWorked: period.daysWorked - }))) + for (const period of periods) { + await WorkPeriodService.partiallyUpdateWorkPeriod(helper.getAuditM2Muser(), + period.id, + { + daysWorked: period.daysWorked + }) + } } /** @@ -285,8 +319,9 @@ async function _updateWorkPeriods (periods) { * @returns {undefined} */ async function _deleteWorkPeriods (workPeriods) { - await Promise.all(_.map(workPeriods, - async workPeriod => await WorkPeriodService.deleteWorkPeriod(helper.getAuditM2Muser(), workPeriod.id))) + for (const period of workPeriods) { + await WorkPeriodService.deleteWorkPeriod(helper.getAuditM2Muser(), period.id) + } } /** diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index e0fac73f..2bd93f4a 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -18,6 +18,7 @@ const moment = require('moment') const ResourceBooking = models.ResourceBooking const WorkPeriod = models.WorkPeriod +const WorkPeriodPayment = models.WorkPeriodPayment const esClient = helper.getESClient() const cachedModelFields = _cacheModelFields() @@ -168,27 +169,41 @@ async function _checkUserPermissionForGetResourceBooking (currentUser, projectId */ async function _ensurePaidWorkPeriodsNotDeleted (resourceBookingId, oldValue, newValue) { function _checkForPaidWorkPeriods (workPeriods) { - const paidWorkPeriods = _.filter(workPeriods - , workPeriod => _.includes(['completed', 'partially-completed'], workPeriod.paymentStatus)) + const paidWorkPeriods = _.filter(workPeriods, workPeriod => { + // filter by WP and WPP status + return (['completed', 'partially-completed', 'in-progress'].indexOf(workPeriod.paymentStatus) !== -1 || + _.some(workPeriod.payments, payment => ['completed', 'in-progress'].indexOf(payment.status) !== -1)) + }) if (paidWorkPeriods.length > 0) { throw new errors.BadRequestError(`WorkPeriods with id of ${_.map(paidWorkPeriods, workPeriod => workPeriod.id)} - has completed or partially-completed payment status.`) + has completed, partially-completed or in-progress payment status.`) } } // find related workPeriods to evaluate the changes - const workPeriods = await WorkPeriod.findAll({ + // We don't need to include WPP because WPP's status changes should + // update WP's status. In case of any bug, it's better to check both WP + // and WPP status for now. + let workPeriods = await WorkPeriod.findAll({ where: { resourceBookingId: resourceBookingId }, - raw: true + attributes: ['id', 'paymentStatus', 'startDate', 'endDate'], + include: [{ + model: WorkPeriodPayment, + as: 'payments', + required: false, + attributes: ['status'] + }] }) + workPeriods = _.map(workPeriods, wp => wp.toJSON()) // oldValue and newValue are not provided at deleteResourceBooking process if (_.isUndefined(oldValue) || _.isUndefined(newValue)) { _checkForPaidWorkPeriods(workPeriods) return } // We should not be able to change status of ResourceBooking to 'cancelled' - // if there is at least one associated Work Period with paymentStatus 'partially-completed' or 'completed'. + // if there is at least one associated Work Period with paymentStatus 'partially-completed', 'completed' or 'in-progress', + // or any of it's WorkPeriodsPayment has status 'completed' or 'in-progress'. if (oldValue.status !== 'cancelled' && newValue.status === 'cancelled') { _checkForPaidWorkPeriods(workPeriods) // we have already checked all existing workPeriods @@ -200,7 +215,8 @@ async function _ensurePaidWorkPeriodsNotDeleted (resourceBookingId, oldValue, ne _.isUndefined(newValue.endDate) ? oldValue.endDate : newValue.endDate) // find which workPeriods should be removed const workPeriodsToRemove = _.differenceBy(workPeriods, newWorkPeriods, 'startDate') - // we can't delete workperiods with paymentStatus 'partially-completed' or 'completed'. + // we can't delete workperiods with paymentStatus 'partially-completed', 'completed' or 'in-progress', + // or any of it's WorkPeriodsPayment has status 'completed' or 'in-progress'. _checkForPaidWorkPeriods(workPeriodsToRemove) } diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index e16ce290..594eb8b1 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -235,7 +235,7 @@ async function createWorkPeriod (currentUser, workPeriod) { } } - await helper.postEvent(config.TAAS_WORK_PERIOD_CREATE_TOPIC, created.toJSON(),{"key":workPeriod.resourceBookingId}) + await helper.postEvent(config.TAAS_WORK_PERIOD_CREATE_TOPIC, created.toJSON(), { key: workPeriod.resourceBookingId }) return created.dataValues } @@ -289,8 +289,8 @@ async function updateWorkPeriod (currentUser, id, data) { } } - //await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) - await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), {oldValue: oldValue, "key":data.resourceBookingId}) + // await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) + await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue, key: data.resourceBookingId }) return updated.dataValues } @@ -356,8 +356,11 @@ async function deleteWorkPeriod (currentUser, id) { } const workPeriod = await WorkPeriod.findById(id, { withPayments: true }) - if (_.includes(['completed', 'partially-completed'], workPeriod.paymentStatus)) { - throw new errors.BadRequestError("Can't delete WorkPeriod with paymentStatus completed or partially-completed") + if (_.includes(['completed', 'partially-completed', 'in-progress'], workPeriod.paymentStatus)) { + throw new errors.BadRequestError("Can't delete WorkPeriod with paymentStatus completed partially-completed, or in-progress") + } + if (_.some(workPeriod.payments, payment => ['completed', 'in-progress'].indexOf(payment.status) !== -1)) { + throw new errors.BadRequestError("Can't delete WorkPeriod if any associated WorkPeriodsPayment has status completed or in-progress") } await models.WorkPeriodPayment.destroy({ where: { diff --git a/test/unit/ResourceBookingService.test.js b/test/unit/ResourceBookingService.test.js index e5052848..a66281b4 100644 --- a/test/unit/ResourceBookingService.test.js +++ b/test/unit/ResourceBookingService.test.js @@ -9,6 +9,7 @@ const commonData = require('./common/CommonData') const testData = require('./common/ResourceBookingData') const helper = require('../../src/common/helper') const errors = require('../../src/common/errors') +const _ = require('lodash') const busApiClient = helper.getBusApiClient() const ResourceBooking = models.ResourceBooking const WorkPeriod = models.WorkPeriod @@ -131,12 +132,15 @@ describe('resourceBooking service test', () => { }) }) describe('Update resource booking successfully', () => { - it('T06:Update resource booking dates and do not cause work period change', async () => { + it('T06:Update resource booking dates and do not cause work period create/delete', async () => { const data = testData.T06 const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { return data.resourceBooking.value }) - const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async () => { + const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async (criteria) => { + if (criteria.raw) { + return _.map(data.workPeriod.response, wp => wp.toJSON()) + } return data.workPeriod.response }) const entity = await service.partiallyUpdateResourceBooking(commonData.currentUser, data.resourceBooking.value.dataValues.id, data.resourceBooking.request) @@ -145,15 +149,20 @@ describe('resourceBooking service test', () => { expect(stubPostEvent.calledOnce).to.be.true expect(stubWorkPeriodFindAll.called).to.be.true expect(stubCreateWorkPeriodService.callCount).to.eq(0) - expect(stubUpdateWorkPeriodService.callCount).to.eq(0) + expect(stubUpdateWorkPeriodService.callCount).to.eq(1) expect(stubDeleteWorkPeriodService.callCount).to.eq(0) + expect(stubUpdateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0].id) + expect(stubUpdateWorkPeriodService.getCall(0).args[2]).to.deep.eq(data.workPeriod.request[0].data) }) it('T07:Update resource booking dates and cause work period creation - 1', async () => { const data = testData.T07 const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { return data.resourceBooking.value }) - const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async () => { + const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async (criteria) => { + if (criteria.raw) { + return _.map(data.workPeriod.response, wp => wp.toJSON()) + } return data.workPeriod.response }) const entity = await service.partiallyUpdateResourceBooking(commonData.currentUser, data.resourceBooking.value.dataValues.id, data.resourceBooking.request) @@ -162,16 +171,21 @@ describe('resourceBooking service test', () => { expect(stubPostEvent.calledOnce).to.be.true expect(stubWorkPeriodFindAll.called).to.be.true expect(stubCreateWorkPeriodService.callCount).to.eq(1) - expect(stubUpdateWorkPeriodService.callCount).to.eq(0) + expect(stubUpdateWorkPeriodService.callCount).to.eq(1) expect(stubDeleteWorkPeriodService.callCount).to.eq(0) - expect(stubCreateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0]) + expect(stubCreateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0].data) + expect(stubUpdateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[1].id) + expect(stubUpdateWorkPeriodService.getCall(0).args[2]).to.deep.eq(data.workPeriod.request[1].data) }) it('T08:Update resource booking dates and cause work period creation - 2', async () => { const data = testData.T08 const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { return data.resourceBooking.value }) - const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async () => { + const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async (criteria) => { + if (criteria.raw) { + return _.map(data.workPeriod.response, wp => wp.toJSON()) + } return data.workPeriod.response }) const entity = await service.partiallyUpdateResourceBooking(commonData.currentUser, data.resourceBooking.value.dataValues.id, data.resourceBooking.request) @@ -180,16 +194,21 @@ describe('resourceBooking service test', () => { expect(stubPostEvent.calledOnce).to.be.true expect(stubWorkPeriodFindAll.called).to.be.true expect(stubCreateWorkPeriodService.callCount).to.eq(1) - expect(stubUpdateWorkPeriodService.callCount).to.eq(0) + expect(stubUpdateWorkPeriodService.callCount).to.eq(1) expect(stubDeleteWorkPeriodService.callCount).to.eq(0) - expect(stubCreateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0]) + expect(stubCreateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0].data) + expect(stubUpdateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[1].id) + expect(stubUpdateWorkPeriodService.getCall(0).args[2]).to.deep.eq(data.workPeriod.request[1].data) }) it('T09:Update resource booking startDate and cause work period to be deleted', async () => { const data = testData.T09 const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { return data.resourceBooking.value }) - const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async () => { + const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async (criteria) => { + if (criteria.raw) { + return _.map(data.workPeriod.response, wp => wp.toJSON()) + } return data.workPeriod.response }) const entity = await service.partiallyUpdateResourceBooking(commonData.currentUser, data.resourceBooking.value.dataValues.id, data.resourceBooking.request) @@ -198,16 +217,21 @@ describe('resourceBooking service test', () => { expect(stubPostEvent.calledOnce).to.be.true expect(stubWorkPeriodFindAll.called).to.be.true expect(stubCreateWorkPeriodService.callCount).to.eq(0) - expect(stubUpdateWorkPeriodService.callCount).to.eq(0) + expect(stubUpdateWorkPeriodService.callCount).to.eq(1) expect(stubDeleteWorkPeriodService.callCount).to.eq(1) - expect(stubDeleteWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0]) + expect(stubDeleteWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0].id) + expect(stubUpdateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[1].id) + expect(stubUpdateWorkPeriodService.getCall(0).args[2]).to.deep.eq(data.workPeriod.request[1].data) }) it('T10:Update resource booking endDate and cause work period to be deleted', async () => { const data = testData.T10 const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { return data.resourceBooking.value }) - const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async () => { + const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async (criteria) => { + if (criteria.raw) { + return _.map(data.workPeriod.response, wp => wp.toJSON()) + } return data.workPeriod.response }) const entity = await service.partiallyUpdateResourceBooking(commonData.currentUser, data.resourceBooking.value.dataValues.id, data.resourceBooking.request) @@ -216,16 +240,21 @@ describe('resourceBooking service test', () => { expect(stubPostEvent.calledOnce).to.be.true expect(stubWorkPeriodFindAll.called).to.be.true expect(stubCreateWorkPeriodService.callCount).to.eq(0) - expect(stubUpdateWorkPeriodService.callCount).to.eq(0) + expect(stubUpdateWorkPeriodService.callCount).to.eq(1) expect(stubDeleteWorkPeriodService.callCount).to.eq(1) - expect(stubDeleteWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0]) + expect(stubDeleteWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0].id) + expect(stubUpdateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[1].id) + expect(stubUpdateWorkPeriodService.getCall(0).args[2]).to.deep.eq(data.workPeriod.request[1].data) }) it('T11:Update resource booking dates and cause work period daysWorked to change', async () => { const data = testData.T11 const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { return data.resourceBooking.value }) - const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async () => { + const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async (criteria) => { + if (criteria.raw) { + return _.map(data.workPeriod.response, wp => wp.toJSON()) + } return data.workPeriod.response }) const entity = await service.partiallyUpdateResourceBooking(commonData.currentUser, data.resourceBooking.value.dataValues.id, data.resourceBooking.request) @@ -234,17 +263,22 @@ describe('resourceBooking service test', () => { expect(stubPostEvent.calledOnce).to.be.true expect(stubWorkPeriodFindAll.called).to.be.true expect(stubCreateWorkPeriodService.callCount).to.eq(0) - expect(stubUpdateWorkPeriodService.callCount).to.eq(1) + expect(stubUpdateWorkPeriodService.callCount).to.eq(2) expect(stubDeleteWorkPeriodService.callCount).to.eq(0) - expect(stubUpdateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0]) - expect(stubUpdateWorkPeriodService.getCall(0).args[2]).to.deep.eq(data.workPeriod.request[1]) + expect(stubUpdateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0].id) + expect(stubUpdateWorkPeriodService.getCall(0).args[2]).to.deep.eq(data.workPeriod.request[0].data) + expect(stubUpdateWorkPeriodService.getCall(1).args[1]).to.deep.eq(data.workPeriod.request[1].id) + expect(stubUpdateWorkPeriodService.getCall(1).args[2]).to.deep.eq(data.workPeriod.request[1].data) }) it('T12:Update resource booking dates and cause delete, update, create work period operations', async () => { const data = testData.T12 const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { return data.resourceBooking.value }) - const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async () => { + const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async (criteria) => { + if (criteria.raw) { + return _.map(data.workPeriod.response, wp => wp.toJSON()) + } return data.workPeriod.response }) const entity = await service.partiallyUpdateResourceBooking(commonData.currentUser, data.resourceBooking.value.dataValues.id, data.resourceBooking.request) @@ -255,10 +289,10 @@ describe('resourceBooking service test', () => { expect(stubCreateWorkPeriodService.callCount).to.eq(1) expect(stubUpdateWorkPeriodService.callCount).to.eq(1) expect(stubDeleteWorkPeriodService.callCount).to.eq(1) - expect(stubDeleteWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0]) - expect(stubUpdateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[1]) - expect(stubUpdateWorkPeriodService.getCall(0).args[2]).to.deep.eq(data.workPeriod.request[2]) - expect(stubCreateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[3]) + expect(stubDeleteWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0].id) + expect(stubUpdateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[1].id) + expect(stubUpdateWorkPeriodService.getCall(0).args[2]).to.deep.eq(data.workPeriod.request[1].data) + expect(stubCreateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[2].data) }) }) describe('Update resource booking unsuccessfully', () => { @@ -267,7 +301,10 @@ describe('resourceBooking service test', () => { const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { return data.resourceBooking.value }) - const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async () => { + const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async (criteria) => { + if (criteria.raw) { + return _.map(data.workPeriod.response, wp => wp.toJSON()) + } return data.workPeriod.response }) let error @@ -290,7 +327,10 @@ describe('resourceBooking service test', () => { const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { return data.resourceBooking.value }) - const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async () => { + const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async (criteria) => { + if (criteria.raw) { + return _.map(data.workPeriod.response, wp => wp.toJSON()) + } return data.workPeriod.response }) let error @@ -315,7 +355,10 @@ describe('resourceBooking service test', () => { const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { return data.resourceBooking.value }) - const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async () => { + const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async (criteria) => { + if (criteria.raw) { + return _.map(data.workPeriod.response, wp => wp.toJSON()) + } return data.workPeriod.response }) await service.deleteResourceBooking(commonData.currentUser, data.resourceBooking.value.dataValues.id) @@ -325,8 +368,8 @@ describe('resourceBooking service test', () => { expect(stubCreateWorkPeriodService.callCount).to.eq(0) expect(stubUpdateWorkPeriodService.callCount).to.eq(0) expect(stubDeleteWorkPeriodService.callCount).to.eq(2) - expect(stubDeleteWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0]) - expect(stubDeleteWorkPeriodService.getCall(1).args[1]).to.deep.eq(data.workPeriod.request[1]) + expect(stubDeleteWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0].id) + expect(stubDeleteWorkPeriodService.getCall(1).args[1]).to.deep.eq(data.workPeriod.request[1].id) }) }) describe('Delete resource booking unsuccessfully', () => { @@ -335,7 +378,10 @@ describe('resourceBooking service test', () => { const stubResourceBookingFindById = sinon.stub(ResourceBooking, 'findById').callsFake(async () => { return data.resourceBooking.value }) - const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async () => { + const stubWorkPeriodFindAll = sinon.stub(WorkPeriod, 'findAll').callsFake(async (criteria) => { + if (criteria.raw) { + return _.map(data.workPeriod.response, wp => wp.toJSON()) + } return data.workPeriod.response }) let error diff --git a/test/unit/common/ResourceBookingData.js b/test/unit/common/ResourceBookingData.js index 18d2ece5..347bd8c5 100644 --- a/test/unit/common/ResourceBookingData.js +++ b/test/unit/common/ResourceBookingData.js @@ -34,42 +34,42 @@ const T01 = { resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', startDate: '2021-03-28', endDate: '2021-04-03', - daysWorked: null, + daysWorked: 0, paymentStatus: 'pending' }, { resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', startDate: '2021-04-04', endDate: '2021-04-10', - daysWorked: null, + daysWorked: 5, paymentStatus: 'pending' }, { resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', startDate: '2021-04-11', endDate: '2021-04-17', - daysWorked: null, + daysWorked: 5, paymentStatus: 'pending' }, { resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', startDate: '2021-04-18', endDate: '2021-04-24', - daysWorked: null, + daysWorked: 5, paymentStatus: 'pending' }, { resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', startDate: '2021-04-25', endDate: '2021-05-01', - daysWorked: null, + daysWorked: 5, paymentStatus: 'pending' }, { resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', startDate: '2021-05-02', endDate: '2021-05-08', - daysWorked: null, + daysWorked: 0, paymentStatus: 'pending' }] } @@ -111,7 +111,7 @@ const T02 = { resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', startDate: '2021-04-11', endDate: '2021-04-17', - daysWorked: null, + daysWorked: 5, paymentStatus: 'pending' }] } @@ -242,21 +242,30 @@ const T06 = { }, workPeriod: { response: [{ - id: '10faf505-d0e3-4d13-a817-7f1319625e91', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-11', - endDate: '2021-04-17', - daysWorked: null, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' - }] + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-11', + endDate: '2021-04-17', + daysWorked: 5, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T06.workPeriod.response[0].dataValues + }], + request: [ + { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + data: { daysWorked: 3 } + } + ] } } T06.resourceBooking.value.toJSON = () => T06.resourceBooking.value.dataValues @@ -305,28 +314,39 @@ const T07 = { }, workPeriod: { response: [{ - id: '10faf505-d0e3-4d13-a817-7f1319625e91', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-11', - endDate: '2021-04-17', - daysWorked: null, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-11', + endDate: '2021-04-17', + daysWorked: 5, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T07.workPeriod.response[0].dataValues }], request: [ { - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - startDate: '2021-04-04', - endDate: '2021-04-10', - daysWorked: null, - paymentStatus: 'pending' + data: { + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + startDate: '2021-04-04', + endDate: '2021-04-10', + daysWorked: 0, + paymentStatus: 'pending' + } + }, + { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + data: { + daysWorked: 4 + } } ] } @@ -377,28 +397,39 @@ const T08 = { }, workPeriod: { response: [{ - id: '10faf505-d0e3-4d13-a817-7f1319625e91', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-11', - endDate: '2021-04-17', - daysWorked: null, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-11', + endDate: '2021-04-17', + daysWorked: 5, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T08.workPeriod.response[0].dataValues }], request: [ { - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - startDate: '2021-04-18', - endDate: '2021-04-24', - daysWorked: null, - paymentStatus: 'pending' + data: { + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + startDate: '2021-04-18', + endDate: '2021-04-24', + daysWorked: 0, + paymentStatus: 'pending' + } + }, + { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + data: { + daysWorked: 5 + } } ] } @@ -449,38 +480,52 @@ const T09 = { }, workPeriod: { response: [{ - id: '10faf505-d0e3-4d13-a817-7f1319625e90', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-04', - endDate: '2021-04-10', - daysWorked: null, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e90', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-04', + endDate: '2021-04-10', + daysWorked: 0, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T09.workPeriod.response[0].dataValues }, { - id: '10faf505-d0e3-4d13-a817-7f1319625e91', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-11', - endDate: '2021-04-17', - daysWorked: null, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-11', + endDate: '2021-04-17', + daysWorked: 5, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T09.workPeriod.response[1].dataValues }], request: [ - '10faf505-d0e3-4d13-a817-7f1319625e90' + { + id: '10faf505-d0e3-4d13-a817-7f1319625e90' + }, + { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + data: { + daysWorked: 4 + } + } ] } } @@ -530,38 +575,52 @@ const T10 = { }, workPeriod: { response: [{ - id: '10faf505-d0e3-4d13-a817-7f1319625e90', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-04', - endDate: '2021-04-10', - daysWorked: null, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e90', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-04', + endDate: '2021-04-10', + daysWorked: 0, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T10.workPeriod.response[0].dataValues }, { - id: '10faf505-d0e3-4d13-a817-7f1319625e91', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-11', - endDate: '2021-04-17', - daysWorked: null, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-11', + endDate: '2021-04-17', + daysWorked: 5, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T10.workPeriod.response[1].dataValues }], request: [ - '10faf505-d0e3-4d13-a817-7f1319625e91' + { + id: '10faf505-d0e3-4d13-a817-7f1319625e91' + }, + { + id: '10faf505-d0e3-4d13-a817-7f1319625e90', + data: { + daysWorked: 2 + } + } ] } } @@ -611,39 +670,51 @@ const T11 = { }, workPeriod: { response: [{ - id: '10faf505-d0e3-4d13-a817-7f1319625e90', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-04', - endDate: '2021-04-10', - daysWorked: 0, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e90', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-04', + endDate: '2021-04-10', + daysWorked: 0, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T11.workPeriod.response[0].dataValues }, { - id: '10faf505-d0e3-4d13-a817-7f1319625e91', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-11', - endDate: '2021-04-17', - daysWorked: 3, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-11', + endDate: '2021-04-17', + daysWorked: 3, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T11.workPeriod.response[1].dataValues }], request: [ - '10faf505-d0e3-4d13-a817-7f1319625e91', - { daysWorked: 2 } + { + id: '10faf505-d0e3-4d13-a817-7f1319625e90', + data: { daysWorked: 2 } + }, + { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + data: { daysWorked: 2 } + } ] } } @@ -693,46 +764,58 @@ const T12 = { }, workPeriod: { response: [{ - id: '10faf505-d0e3-4d13-a817-7f1319625e90', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-04', - endDate: '2021-04-10', - daysWorked: 4, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e90', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-04', + endDate: '2021-04-10', + daysWorked: 4, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T12.workPeriod.response[0].dataValues }, { - id: '10faf505-d0e3-4d13-a817-7f1319625e91', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-11', - endDate: '2021-04-17', - daysWorked: 4, - memberRate: null, - customerRate: null, - paymentStatus: 'partially-completed', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-11', + endDate: '2021-04-17', + daysWorked: 4, + memberRate: null, + customerRate: null, + paymentStatus: 'partially-completed', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T12.workPeriod.response[1].dataValues }], request: [ - '10faf505-d0e3-4d13-a817-7f1319625e90', - '10faf505-d0e3-4d13-a817-7f1319625e91', - { daysWorked: 3 }, { - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - startDate: '2021-04-18', - endDate: '2021-04-24', - daysWorked: null, - paymentStatus: 'pending' + id: '10faf505-d0e3-4d13-a817-7f1319625e90' + }, + { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + data: { daysWorked: 3 } + }, + { + data: { + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + startDate: '2021-04-18', + endDate: '2021-04-24', + daysWorked: 5, + paymentStatus: 'pending' + } } ] } @@ -766,39 +849,45 @@ const T13 = { error: { httpStatus: 400, message: `WorkPeriods with id of 10faf505-d0e3-4d13-a817-7f1319625e91 - has completed or partially-completed payment status.` + has completed, partially-completed or in-progress payment status.` }, workPeriod: { response: [{ - id: '10faf505-d0e3-4d13-a817-7f1319625e90', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-04', - endDate: '2021-04-10', - daysWorked: 4, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e90', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-04', + endDate: '2021-04-10', + daysWorked: 4, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T13.workPeriod.response[0].dataValues }, { - id: '10faf505-d0e3-4d13-a817-7f1319625e91', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-11', - endDate: '2021-04-17', - daysWorked: 4, - memberRate: null, - customerRate: null, - paymentStatus: 'completed', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-11', + endDate: '2021-04-17', + daysWorked: 4, + memberRate: null, + customerRate: null, + paymentStatus: 'completed', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T13.workPeriod.response[1].dataValues }] } } @@ -830,39 +919,45 @@ const T14 = { error: { httpStatus: 400, message: `WorkPeriods with id of 10faf505-d0e3-4d13-a817-7f1319625e91 - has completed or partially-completed payment status.` + has completed, partially-completed or in-progress payment status.` }, workPeriod: { response: [{ - id: '10faf505-d0e3-4d13-a817-7f1319625e90', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-04', - endDate: '2021-04-10', - daysWorked: 4, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e90', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-04', + endDate: '2021-04-10', + daysWorked: 4, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T14.workPeriod.response[0].dataValues }, { - id: '10faf505-d0e3-4d13-a817-7f1319625e91', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-11', - endDate: '2021-04-17', - daysWorked: 4, - memberRate: null, - customerRate: null, - paymentStatus: 'completed', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-11', + endDate: '2021-04-17', + daysWorked: 4, + memberRate: null, + customerRate: null, + paymentStatus: 'completed', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T14.workPeriod.response[1].dataValues }] } } @@ -889,37 +984,50 @@ const T15 = { }, workPeriod: { response: [{ - id: '10faf505-d0e3-4d13-a817-7f1319625e90', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-04', - endDate: '2021-04-10', - daysWorked: 4, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e90', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-04', + endDate: '2021-04-10', + daysWorked: 4, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T15.workPeriod.response[0].dataValues }, { - id: '10faf505-d0e3-4d13-a817-7f1319625e91', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-11', - endDate: '2021-04-17', - daysWorked: 4, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-11', + endDate: '2021-04-17', + daysWorked: 4, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T15.workPeriod.response[1].dataValues }], - request: ['10faf505-d0e3-4d13-a817-7f1319625e90', '10faf505-d0e3-4d13-a817-7f1319625e91'] + request: [ + { + id: '10faf505-d0e3-4d13-a817-7f1319625e90' + }, + { + id: '10faf505-d0e3-4d13-a817-7f1319625e91' + } + ] } } T15.resourceBooking.value.toJSON = () => T15.resourceBooking.value.dataValues @@ -947,39 +1055,45 @@ const T16 = { error: { httpStatus: 400, message: `WorkPeriods with id of 10faf505-d0e3-4d13-a817-7f1319625e91 - has completed or partially-completed payment status.` + has completed, partially-completed or in-progress payment status.` }, workPeriod: { response: [{ - id: '10faf505-d0e3-4d13-a817-7f1319625e90', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-04', - endDate: '2021-04-10', - daysWorked: 4, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e90', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-04', + endDate: '2021-04-10', + daysWorked: 4, + memberRate: null, + customerRate: null, + paymentStatus: 'pending', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T16.workPeriod.response[0].dataValues }, { - id: '10faf505-d0e3-4d13-a817-7f1319625e91', - resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', - userHandle: 'pshah_manager', - projectId: 21, - startDate: '2021-04-11', - endDate: '2021-04-17', - daysWorked: 4, - memberRate: null, - customerRate: null, - paymentStatus: 'completed', - createdBy: '00000000-0000-0000-0000-000000000000', - updatedBy: null, - createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + dataValues: { + id: '10faf505-d0e3-4d13-a817-7f1319625e91', + resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', + userHandle: 'pshah_manager', + projectId: 21, + startDate: '2021-04-11', + endDate: '2021-04-17', + daysWorked: 4, + memberRate: null, + customerRate: null, + paymentStatus: 'completed', + createdBy: '00000000-0000-0000-0000-000000000000', + updatedBy: null, + createdAt: '2021-04-10T22:25:08.289Z', + updatedAt: '2021-04-10T22:25:08.289Z' + }, + toJSON: () => T16.workPeriod.response[1].dataValues }] } } From 01c6595ca5582f6a426fc6dd73a2c3ea2b5f7fde Mon Sep 17 00:00:00 2001 From: eisbilir Date: Sat, 12 Jun 2021 01:12:53 +0300 Subject: [PATCH 67/86] daysWorked constraint --- src/models/WorkPeriod.js | 3 ++- src/services/WorkPeriodService.js | 26 ++++++++++++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/models/WorkPeriod.js b/src/models/WorkPeriod.js index 75ff0597..95557368 100644 --- a/src/models/WorkPeriod.js +++ b/src/models/WorkPeriod.js @@ -78,7 +78,8 @@ module.exports = (sequelize) => { }, daysWorked: { field: 'days_worked', - type: Sequelize.INTEGER + type: Sequelize.INTEGER, + allowNull: false }, memberRate: { field: 'member_rate', diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index 437ae571..e94896c8 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -245,7 +245,7 @@ createWorkPeriod.schema = Joi.object().keys({ resourceBookingId: Joi.string().uuid().required(), startDate: Joi.workPeriodStartDate(), endDate: Joi.workPeriodEndDate(), - daysWorked: Joi.number().integer().min(0).allow(null), + daysWorked: Joi.number().integer().min(0).max(5).required(), memberRate: Joi.number().allow(null), customerRate: Joi.number().allow(null), paymentStatus: Joi.paymentStatus().required() @@ -265,18 +265,32 @@ async function updateWorkPeriod (currentUser, id, data) { const workPeriod = await WorkPeriod.findById(id) const oldValue = workPeriod.toJSON() - + let resourceBooking // if resourceBookingId is provided then update projectId and userHandle if (data.resourceBookingId) { - const resourceBooking = await helper.ensureResourceBookingById(data.resourceBookingId) // ensure resource booking exists + resourceBooking = await helper.ensureResourceBookingById(data.resourceBookingId) // ensure resource booking exists data.projectId = resourceBooking.projectId const user = await helper.ensureUserById(resourceBooking.userId) // ensure user exists data.userHandle = user.handle + } else { + resourceBooking = await helper.ensureResourceBookingById(oldValue.resourceBookingId) } // If one of the dates are missing then auto-calculate it _autoCalculateDates(data) - + if (data.daysWorked) { + const weeks = helper.extractWorkPeriods(resourceBooking.startDate, resourceBooking.endDate) + if (_.isEmpty(weeks)) { + throw new errors.ConflictError('Resource booking has missing dates') + } + const thisWeek = _.find(weeks, ['startDate', oldValue.startDate]) + if (_.isNil(thisWeek)) { + throw new errors.ConflictError('Work Period dates are not compatible with Resource Booking dates') + } + if (thisWeek.daysWorked < data.daysWorked) { + throw new errors.BadRequestError(`Maximum allowed daysWorked is (${thisWeek.daysWorked})`) + } + } data.updatedBy = await helper.getUserId(currentUser.userId) let updated = null try { @@ -311,7 +325,7 @@ partiallyUpdateWorkPeriod.schema = Joi.object().keys({ resourceBookingId: Joi.string().uuid(), startDate: Joi.workPeriodStartDate(), endDate: Joi.workPeriodEndDateOptional(), - daysWorked: Joi.number().integer().min(0).allow(null), + daysWorked: Joi.number().integer().min(0).max(5), memberRate: Joi.number().allow(null), customerRate: Joi.number().allow(null), paymentStatus: Joi.paymentStatus() @@ -336,7 +350,7 @@ fullyUpdateWorkPeriod.schema = Joi.object().keys({ resourceBookingId: Joi.string().uuid().required(), startDate: Joi.workPeriodStartDate(), endDate: Joi.workPeriodEndDate(), - daysWorked: Joi.number().integer().min(0).allow(null).default(null), + daysWorked: Joi.number().integer().min(0).required(), memberRate: Joi.number().allow(null).default(null), customerRate: Joi.number().allow(null).default(null), paymentStatus: Joi.paymentStatus().required() From fb3bcc8e467a5d7fb2d8beaa27378c8158d16e6f Mon Sep 17 00:00:00 2001 From: xxcxy Date: Sat, 12 Jun 2021 15:10:46 +0800 Subject: [PATCH 68/86] fix search WorkPeriod --- src/services/WorkPeriodService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index 71108e46..648d2477 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -519,7 +519,7 @@ async function searchWorkPeriods (currentUser, criteria, options = { returnAll: required: false }] } - const workPeriods = await WorkPeriod.findAndCountAll(queryCriteria) + const workPeriods = await WorkPeriod.findAll(queryCriteria) const total = await WorkPeriod.count({ where: filter }) return { fromDb: true, From 216c25636e413d2522d12891a3e416eeee2dd264 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Sun, 13 Jun 2021 15:23:48 +0300 Subject: [PATCH 69/86] clear unnecessary function call --- src/services/TeamService.js | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 28ec014a..af006603 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -764,9 +764,7 @@ async function roleSearchRequest (currentUser, data) { } 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')) + const skills = _.map(tags, 'tag') // find the best matching role role = await getRoleBySkills(skills) } @@ -930,27 +928,6 @@ getSkillIdsByNames.schema = Joi.object() 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 * @@ -1156,7 +1133,6 @@ module.exports = { getSkillsByJobDescription, getSkillNamesByIds, getSkillIdsByNames, - getSkillNamesByNames, createRoleSearchRequest, isExternalMember, createTeam, From 8bb17e5a5ac65dac51e5966ed130a4d9f94aa5da Mon Sep 17 00:00:00 2001 From: eisbilir Date: Mon, 14 Jun 2021 02:50:52 +0300 Subject: [PATCH 70/86] wp-wpp update and automation --- app-constants.js | 6 + data/demo-data.json | 7471 +++++++++-------- ...coder-bookings-api.postman_collection.json | 4975 +++-------- docs/swagger.yaml | 410 +- ...late-work-periods-for-resource-bookings.js | 145 + src/bootstrap.js | 5 +- src/common/helper.js | 7 +- src/controllers/WorkPeriodController.js | 32 - .../WorkPeriodPaymentController.js | 10 - .../ResourceBookingEventHandler.js | 16 +- .../WorkPeriodPaymentEventHandler.js | 87 + src/eventHandlers/index.js | 3 + src/models/WorkPeriod.js | 14 +- src/models/WorkPeriodPayment.js | 19 +- src/routes/WorkPeriodPaymentRoutes.js | 6 - src/routes/WorkPeriodRoutes.js | 18 - src/services/ResourceBookingService.js | 24 +- src/services/WorkPeriodPaymentService.js | 133 +- src/services/WorkPeriodService.js | 131 +- test/unit/ResourceBookingService.test.js | 32 +- test/unit/WorkPeriodPaymentService.test.js | 16 +- test/unit/common/ResourceBookingData.js | 100 +- test/unit/common/WorkPeriodPaymentData.js | 145 +- 23 files changed, 5918 insertions(+), 7887 deletions(-) create mode 100644 migrations/2021-06-14-create-and-populate-work-periods-for-resource-bookings.js create mode 100644 src/eventHandlers/WorkPeriodPaymentEventHandler.js diff --git a/app-constants.js b/app-constants.js index 2c232275..701df82d 100644 --- a/app-constants.js +++ b/app-constants.js @@ -92,6 +92,11 @@ const WorkPeriodPaymentStatus = { CANCELLED: 'cancelled' } +const WorkPeriodPaymentUpdateStatus = { + SCHEDULED: 'scheduled', + CANCELLED: 'cancelled' +} + const PaymentProcessingSwitch = { ON: 'ON', OFF: 'OFF' @@ -113,6 +118,7 @@ module.exports = { Interviews, ChallengeStatus, WorkPeriodPaymentStatus, + WorkPeriodPaymentUpdateStatus, PaymentSchedulerStatus, PaymentProcessingSwitch } diff --git a/data/demo-data.json b/data/demo-data.json index 19f5f8ef..f390ef15 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -149,6 +149,13 @@ ], "status": "closed", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "fe38eed1-af73-41fd-85a2-ac4da1ff09a3", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-03-19T08:11:26.966Z", @@ -171,6 +178,13 @@ ], "status": "sourcing", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "71c5e6a8-51d9-4fb5-91ce-d974642531af", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-27T06:28:58.039Z", @@ -191,6 +205,13 @@ "skills": [], "status": "assigned", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2020-12-14T12:41:14.323Z", @@ -214,6 +235,13 @@ ], "status": "sourcing", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-13T08:51:26.517Z", @@ -236,6 +264,13 @@ ], "status": "sourcing", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "077fd578-7463-457f-b6ef-22c02178d7f5", "updatedBy": null, "createdAt": "2021-02-21T09:26:04.620Z", @@ -261,6 +296,13 @@ ], "status": "sourcing", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "077fd578-7463-457f-b6ef-22c02178d7f5", "createdAt": "2021-02-04T10:25:20.358Z", @@ -281,6 +323,13 @@ "skills": [], "status": "sourcing", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", "updatedBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", "createdAt": "2021-01-31T20:29:34.581Z", @@ -306,6 +355,13 @@ ], "status": "in-review", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", "updatedBy": "077fd578-7463-457f-b6ef-22c02178d7f5", "createdAt": "2021-01-11T08:29:29.153Z", @@ -328,6 +384,13 @@ ], "status": "in-review", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "3f64739e-10bf-42ca-8314-8aea0245cd0f", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-04-30T07:56:22.940Z", @@ -353,6 +416,13 @@ ], "status": "sourcing", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", "updatedBy": null, "createdAt": "2021-01-07T19:42:10.255Z", @@ -388,6 +458,13 @@ ], "status": "sourcing", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", "updatedBy": null, "createdAt": "2020-12-08T15:56:18.707Z", @@ -415,6 +492,13 @@ ], "status": "sourcing", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": null, "createdAt": "2021-01-12T08:57:01.987Z", @@ -437,6 +521,13 @@ ], "status": "in-review", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "fe38eed1-af73-41fd-85a2-ac4da1ff09a3", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-25T11:03:00.164Z", @@ -459,6 +550,13 @@ ], "status": "in-review", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "3f64739e-10bf-42ca-8314-8aea0245cd0f", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-01T09:43:13.357Z", @@ -497,6 +595,13 @@ ], "status": "sourcing", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", "updatedBy": null, "createdAt": "2020-12-07T09:30:37.470Z", @@ -522,6 +627,13 @@ ], "status": "cancelled", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2020-12-24T06:48:56.943Z", @@ -544,6 +656,13 @@ ], "status": "sourcing", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", "updatedBy": null, "createdAt": "2020-11-20T05:30:03.014Z", @@ -569,6 +688,13 @@ ], "status": "sourcing", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", "updatedBy": null, "createdAt": "2020-12-08T13:50:26.805Z", @@ -594,6 +720,13 @@ ], "status": "sourcing", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "71c5e6a8-51d9-4fb5-91ce-d974642531af", "updatedBy": null, "createdAt": "2021-02-18T09:43:11.985Z", @@ -614,6 +747,13 @@ "skills": [], "status": "sourcing", "isApplicationPageActive": false, + "minSalary": null, + "maxSalary": null, + "hoursPerWeek": null, + "jobLocation": null, + "jobTimezone": null, + "currency": null, + "roleIds": null, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "077fd578-7463-457f-b6ef-22c02178d7f5", "createdAt": "2021-01-25T10:07:56.847Z", @@ -1217,1448 +1357,1291 @@ ], "ResourceBooking": [ { - "id": "c0a12936-77ef-46fa-8c75-6996339e79f6", + "id": "d6103727-6615-4168-8169-0485577bfb3f", "projectId": 111, - "userId": "05e988b7-7d54-4c10-ada1-1a04870a88a8", + "userId": "bef43122-426b-4b2b-acdd-9b5b3bd1c0bf", "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", "status": "placed", - "startDate": "2020-09-27", - "endDate": "2020-10-27", + "startDate": "2021-03-27", + "endDate": "2021-04-27", "memberRate": 13.23, "customerRate": 13, "rateType": "hourly", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:25:46.728Z", - "updatedAt": "2021-05-09T21:25:46.728Z", + "createdAt": "2021-05-09T21:27:11.914Z", + "updatedAt": "2021-05-09T21:27:11.914Z", "workPeriods": [ { - "id": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", - "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", - "userHandle": "sachin-wipro", + "id": "5904b1d9-cb50-4b5d-8103-6741fec2f86b", + "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", + "userHandle": "testcat", "projectId": 111, - "startDate": "2020-09-27", - "endDate": "2020-10-03", - "daysWorked": 4, - "memberRate": 27.06, - "customerRate": 13.13, - "paymentStatus": "partially-completed", + "startDate": "2021-04-18", + "endDate": "2021-04-24", + "daysWorked": 2, + "daysPaid": 2, + "paymentTotal": 9.13, + "paymentStatus": "completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:25:47.813Z", - "updatedAt": "2021-05-09T21:45:32.659Z", + "createdAt": "2021-05-09T21:27:12.746Z", + "updatedAt": "2021-05-09T21:47:44.291Z", "payments": [ { - "id": "1c682ea9-ba63-4fcc-b00c-049d2458d3ac", - "workPeriodId": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 57.79, - "status": "completed", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:31:35.726Z", - "updatedAt": "2021-05-09T21:31:35.726Z" - }, - { - "id": "14b266c6-e76a-4042-b439-74fe3e42c90f", - "workPeriodId": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", + "id": "dfc9bed6-78f2-407e-a7e4-abea9a3d3b46", + "workPeriodId": "5904b1d9-cb50-4b5d-8103-6741fec2f86b", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 417.42, + "memberRate": 22.82, + "customerRate": 4.07, + "days": 2, + "amount": 9.13, "status": "cancelled", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:38.183Z", - "updatedAt": "2021-05-09T21:31:38.183Z" + "createdAt": "2021-05-09T21:32:27.398Z", + "updatedAt": "2021-05-09T21:32:27.398Z" }, { - "id": "03a0163c-472e-4ea6-b8ad-3dc86d418ecf", - "workPeriodId": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", + "id": "be4d5099-8b8e-45e2-b6cd-ab1997f57e26", + "workPeriodId": "5904b1d9-cb50-4b5d-8103-6741fec2f86b", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 210.19, - "status": "cancelled", + "memberRate": 22.82, + "customerRate": 4.07, + "days": 2, + "amount": 9.13, + "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:36.932Z", - "updatedAt": "2021-05-09T21:31:36.932Z" + "createdAt": "2021-05-09T21:32:26.174Z", + "updatedAt": "2021-05-09T21:32:26.174Z" } ] }, { - "id": "e8346d7b-4ada-428d-a768-c2989306f63a", - "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", - "userHandle": "sachin-wipro", + "id": "d111a56f-593d-452e-9787-551bea504c92", + "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", + "userHandle": "testcat", "projectId": 111, - "startDate": "2020-10-18", - "endDate": "2020-10-24", + "startDate": "2021-04-04", + "endDate": "2021-04-10", "daysWorked": 4, - "memberRate": 4.08, - "customerRate": 3.89, - "paymentStatus": "cancelled", + "daysPaid": 3, + "paymentTotal": 13.92, + "paymentStatus": "partially-completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:25:47.834Z", - "updatedAt": "2021-05-09T21:45:37.647Z", + "createdAt": "2021-05-09T21:27:12.731Z", + "updatedAt": "2021-05-09T21:48:08.381Z", "payments": [ { - "id": "cc235aee-0911-4869-bb49-911507bb31e7", - "workPeriodId": "e8346d7b-4ada-428d-a768-c2989306f63a", + "id": "abb79afc-a370-4625-a067-a3b57c9b4700", + "workPeriodId": "d111a56f-593d-452e-9787-551bea504c92", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 494.46, - "status": "cancelled", + "memberRate": 23.2, + "customerRate": 30.71, + "days": 2, + "amount": 9.28, + "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:26.807Z", - "updatedAt": "2021-05-09T21:34:26.807Z" + "createdAt": "2021-05-09T21:34:10.371Z", + "updatedAt": "2021-05-09T21:34:10.371Z" }, { - "id": "e8f3d379-f5a0-47f6-b37b-cae24f5909e9", - "workPeriodId": "e8346d7b-4ada-428d-a768-c2989306f63a", + "id": "fa9bd31c-6c83-4ee4-9d45-a833cfe821f5", + "workPeriodId": "d111a56f-593d-452e-9787-551bea504c92", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 494.46, + "memberRate": 23.2, + "customerRate": 30.71, + "days": 1, + "amount": 4.64, "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:22.403Z", - "updatedAt": "2021-05-09T21:34:22.403Z" + "createdAt": "2021-05-09T21:34:11.662Z", + "updatedAt": "2021-05-09T21:34:11.662Z" } ] }, { - "id": "a3bbd01d-535d-4f02-8524-0d61395b84e9", - "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", - "userHandle": "sachin-wipro", + "id": "061f31fb-4f8c-462f-92c2-e5d275c45fde", + "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", + "userHandle": "testcat", "projectId": 111, - "startDate": "2020-10-25", - "endDate": "2020-10-31", - "daysWorked": 3, - "memberRate": 15.61, - "customerRate": 9.76, - "paymentStatus": "pending", + "startDate": "2021-03-28", + "endDate": "2021-04-03", + "daysWorked": 2, + "daysPaid": 2, + "paymentTotal": 3.24, + "paymentStatus": "completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:25:47.824Z", - "updatedAt": "2021-05-09T21:45:48.727Z", + "createdAt": "2021-05-09T21:27:12.733Z", + "updatedAt": "2021-05-09T21:47:38.022Z", "payments": [ { - "id": "7c27346e-a23f-46f3-b4d5-88a323fd437d", - "workPeriodId": "a3bbd01d-535d-4f02-8524-0d61395b84e9", + "id": "3770680c-8045-43d4-8baf-cb7b3b714d39", + "workPeriodId": "061f31fb-4f8c-462f-92c2-e5d275c45fde", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 8.09, + "customerRate": 22.15, + "days": 2, + "amount": 3.24, + "status": "cancelled", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:30:42.711Z", + "updatedAt": "2021-05-09T21:30:42.711Z" + }, + { + "id": "c658d66e-86e1-49c7-8051-2b9a017935ad", + "workPeriodId": "061f31fb-4f8c-462f-92c2-e5d275c45fde", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 210.19, + "memberRate": 8.09, + "customerRate": 22.15, + "days": 2, + "amount": 3.24, "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:46.507Z", - "updatedAt": "2021-05-09T21:33:46.507Z" + "createdAt": "2021-05-09T21:30:41.304Z", + "updatedAt": "2021-05-09T21:30:41.304Z" } ] }, { - "id": "fa030947-2f2f-4976-82ea-0a22ee96635a", - "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", - "userHandle": "sachin-wipro", + "id": "6adbf80c-43aa-41ad-b7db-544dc76f9b1c", + "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", + "userHandle": "testcat", "projectId": 111, - "startDate": "2020-10-11", - "endDate": "2020-10-17", + "startDate": "2021-04-25", + "endDate": "2021-05-01", + "daysWorked": 0, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "noDays", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:12.794Z", + "updatedAt": "2021-05-09T21:47:51.348Z", + "payments": [] + }, + { + "id": "6ed56eb5-cadc-45f8-bbdf-3f408948c274", + "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", + "userHandle": "testcat", + "projectId": 111, + "startDate": "2021-03-21", + "endDate": "2021-03-27", "daysWorked": 4, - "memberRate": 10.82, - "customerRate": 30.71, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:25:47.815Z", - "updatedAt": "2021-05-09T21:45:41.810Z", + "createdAt": "2021-05-09T21:27:12.737Z", + "updatedAt": "2021-05-09T21:47:58.216Z", "payments": [ { - "id": "2a4d1488-d27b-4bc8-af68-1a2b893774de", - "workPeriodId": "fa030947-2f2f-4976-82ea-0a22ee96635a", + "id": "ea3bdc7a-7c14-4ac1-955d-2540589fcfa6", + "workPeriodId": "6ed56eb5-cadc-45f8-bbdf-3f408948c274", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 477.97, - "status": "completed", + "memberRate": 22.85, + "customerRate": 29.89, + "days": 2, + "amount": 9.14, + "status": "cancelled", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:31.474Z", - "updatedAt": "2021-05-09T21:34:31.474Z" + "createdAt": "2021-05-09T21:33:00.899Z", + "updatedAt": "2021-05-09T21:33:00.899Z" + }, + { + "id": "3bf29da2-581c-4f9c-8b0f-ff3c876848a0", + "workPeriodId": "6ed56eb5-cadc-45f8-bbdf-3f408948c274", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 22.85, + "customerRate": 29.89, + "days": 1, + "amount": 4.57, + "status": "cancelled", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:59.500Z", + "updatedAt": "2021-05-09T21:32:59.500Z" } ] }, { - "id": "61c1e7e3-5e0a-4892-9099-872bc4c11a22", - "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", - "userHandle": "sachin-wipro", + "id": "0c825dec-6e7b-4dde-943a-f3f8354219cc", + "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", + "userHandle": "testcat", "projectId": 111, - "startDate": "2020-10-04", - "endDate": "2020-10-10", + "startDate": "2021-04-11", + "endDate": "2021-04-17", "daysWorked": 4, - "memberRate": 16.71, - "customerRate": 24.11, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:25:47.841Z", - "updatedAt": "2021-05-09T21:45:27.504Z", + "createdAt": "2021-05-09T21:27:12.783Z", + "updatedAt": "2021-05-09T21:48:03.740Z", "payments": [ { - "id": "40e862a0-8772-4587-88b4-23acff8eb2e0", - "workPeriodId": "61c1e7e3-5e0a-4892-9099-872bc4c11a22", + "id": "78641310-ad51-40a3-a0fd-fdd8d15455b9", + "workPeriodId": "0c825dec-6e7b-4dde-943a-f3f8354219cc", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 417.42, + "memberRate": 5.59, + "customerRate": 30.54, + "days": 3, + "amount": 3.35, "status": "cancelled", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:40.091Z", - "updatedAt": "2021-05-09T21:32:40.091Z" + "createdAt": "2021-05-09T21:30:50.460Z", + "updatedAt": "2021-05-09T21:30:50.460Z" }, { - "id": "fcd10a26-3548-4f9b-9e2b-20397d057800", - "workPeriodId": "61c1e7e3-5e0a-4892-9099-872bc4c11a22", + "id": "5bdd5c22-d9b7-428c-b084-d0950a18bc37", + "workPeriodId": "0c825dec-6e7b-4dde-943a-f3f8354219cc", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 494.46, + "memberRate": 5.59, + "customerRate": 30.54, + "days": 4, + "amount": 4.47, "status": "cancelled", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:41.381Z", - "updatedAt": "2021-05-09T21:32:41.381Z" + "createdAt": "2021-05-09T21:30:49.123Z", + "updatedAt": "2021-05-09T21:30:49.123Z" } ] } ] }, { - "id": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "id": "c0a12936-77ef-46fa-8c75-6996339e79f6", "projectId": 111, - "userId": "6d0509c7-5f12-4d84-9a19-8e80ef7ddd66", + "userId": "05e988b7-7d54-4c10-ada1-1a04870a88a8", "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", - "status": "closed", - "startDate": "2021-03-27", - "endDate": "2021-04-27", + "status": "placed", + "startDate": "2020-09-27", + "endDate": "2020-10-27", "memberRate": 13.23, "customerRate": 13, "rateType": "hourly", "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:26:16.403Z", - "updatedAt": "2021-05-09T21:26:16.403Z", + "createdAt": "2021-05-09T21:25:46.728Z", + "updatedAt": "2021-05-09T21:25:46.728Z", "workPeriods": [ { - "id": "b0758857-0221-47a5-a444-e263e5d9e1cf", - "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", - "userHandle": "aaaa", + "id": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", + "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", + "userHandle": "sachin-wipro", "projectId": 111, - "startDate": "2021-03-21", - "endDate": "2021-03-27", - "daysWorked": 5, - "memberRate": 27.06, - "customerRate": 22.77, - "paymentStatus": "pending", + "startDate": "2020-09-27", + "endDate": "2020-10-03", + "daysWorked": 4, + "daysPaid": 3, + "paymentTotal": 16.24, + "paymentStatus": "partially-completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:26:17.187Z", - "updatedAt": "2021-05-09T21:46:59.354Z", + "createdAt": "2021-05-09T21:25:47.813Z", + "updatedAt": "2021-05-09T21:45:32.659Z", "payments": [ { - "id": "9785ae89-05dc-4bcc-a030-52bd0e681d41", - "workPeriodId": "b0758857-0221-47a5-a444-e263e5d9e1cf", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 168.54, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:34:01.410Z", - "updatedAt": "2021-05-09T21:34:01.410Z" - }, - { - "id": "c8be508d-2eb5-4712-8bd7-1b28e870abc2", - "workPeriodId": "b0758857-0221-47a5-a444-e263e5d9e1cf", + "id": "1c682ea9-ba63-4fcc-b00c-049d2458d3ac", + "workPeriodId": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, + "memberRate": 27.06, + "customerRate": 13.13, + "days": 3, + "amount": 16.24, "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:02.733Z", - "updatedAt": "2021-05-09T21:34:02.733Z" - } - ] - }, - { - "id": "176db0d0-474f-4590-831a-547d596c01b4", - "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", - "userHandle": "aaaa", - "projectId": 111, - "startDate": "2021-03-28", - "endDate": "2021-04-03", - "daysWorked": 5, - "memberRate": 6.11, - "customerRate": 14.63, - "paymentStatus": "cancelled", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:26:17.168Z", - "updatedAt": "2021-05-09T21:47:05.373Z", - "payments": [ + "createdAt": "2021-05-09T21:31:35.726Z", + "updatedAt": "2021-05-09T21:31:35.726Z" + }, { - "id": "3ed31706-0e99-4084-81f4-b126a1a68db6", - "workPeriodId": "176db0d0-474f-4590-831a-547d596c01b4", + "id": "14b266c6-e76a-4042-b439-74fe3e42c90f", + "workPeriodId": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 144.16, - "status": "completed", + "memberRate": 27.06, + "customerRate": 13.13, + "days": 4, + "amount": 21.65, + "status": "cancelled", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:17.193Z", - "updatedAt": "2021-05-09T21:31:17.193Z" + "createdAt": "2021-05-09T21:31:38.183Z", + "updatedAt": "2021-05-09T21:31:38.183Z" }, { - "id": "c3b71f96-7680-459b-82b2-f6eb3c3f6c8f", - "workPeriodId": "176db0d0-474f-4590-831a-547d596c01b4", + "id": "03a0163c-472e-4ea6-b8ad-3dc86d418ecf", + "workPeriodId": "1cdd1505-f6f4-40f6-acce-da7a4578dab5", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 119.32, - "status": "completed", + "memberRate": 27.06, + "customerRate": 13.13, + "days": 4, + "amount": 21.65, + "status": "cancelled", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:18.342Z", - "updatedAt": "2021-05-09T21:31:18.342Z" + "createdAt": "2021-05-09T21:31:36.932Z", + "updatedAt": "2021-05-09T21:31:36.932Z" } ] }, { - "id": "5a174833-cb08-49f5-b077-cffb8e60ca01", - "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", - "userHandle": "aaaa", + "id": "61c1e7e3-5e0a-4892-9099-872bc4c11a22", + "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", + "userHandle": "sachin-wipro", "projectId": 111, - "startDate": "2021-04-04", - "endDate": "2021-04-10", - "daysWorked": 5, - "memberRate": 4.91, - "customerRate": 24.11, + "startDate": "2020-10-04", + "endDate": "2020-10-10", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:26:17.165Z", - "updatedAt": "2021-05-09T21:47:12.015Z", + "createdAt": "2021-05-09T21:25:47.841Z", + "updatedAt": "2021-05-09T21:45:27.504Z", "payments": [ { - "id": "663f11df-7832-431a-a46e-ad8c890ae52b", - "workPeriodId": "5a174833-cb08-49f5-b077-cffb8e60ca01", + "id": "40e862a0-8772-4587-88b4-23acff8eb2e0", + "workPeriodId": "61c1e7e3-5e0a-4892-9099-872bc4c11a22", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 55.6, + "memberRate": 16.71, + "customerRate": 24.11, + "days": 4, + "amount": 13.37, "status": "cancelled", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:32.606Z", - "updatedAt": "2021-05-09T21:32:32.606Z" + "createdAt": "2021-05-09T21:32:40.091Z", + "updatedAt": "2021-05-09T21:32:40.091Z" }, { - "id": "69445cbf-6d94-49a5-b2aa-65459ec78594", - "workPeriodId": "5a174833-cb08-49f5-b077-cffb8e60ca01", + "id": "fcd10a26-3548-4f9b-9e2b-20397d057800", + "workPeriodId": "61c1e7e3-5e0a-4892-9099-872bc4c11a22", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 417.42, - "status": "completed", + "memberRate": 16.71, + "customerRate": 24.11, + "days": 2, + "amount": 6.68, + "status": "cancelled", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:33.664Z", - "updatedAt": "2021-05-09T21:32:33.664Z" + "createdAt": "2021-05-09T21:32:41.381Z", + "updatedAt": "2021-05-09T21:32:41.381Z" } ] }, { - "id": "8c9db4fd-78ad-4e59-acba-462487b74c3a", - "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", - "userHandle": "aaaa", + "id": "e8346d7b-4ada-428d-a768-c2989306f63a", + "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", + "userHandle": "sachin-wipro", "projectId": 111, - "startDate": "2021-04-18", - "endDate": "2021-04-24", - "daysWorked": 2, - "memberRate": 27.46, - "customerRate": 25.57, + "startDate": "2020-10-18", + "endDate": "2020-10-24", + "daysWorked": 4, + "daysPaid": 4, + "paymentTotal": 3.26, "paymentStatus": "completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:26:17.200Z", - "updatedAt": "2021-05-09T21:47:25.687Z", + "createdAt": "2021-05-09T21:25:47.834Z", + "updatedAt": "2021-05-09T21:45:37.647Z", "payments": [ { - "id": "f65930b7-d61d-4923-bdab-54848661f151", - "workPeriodId": "8c9db4fd-78ad-4e59-acba-462487b74c3a", + "id": "cc235aee-0911-4869-bb49-911507bb31e7", + "workPeriodId": "e8346d7b-4ada-428d-a768-c2989306f63a", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 57.79, - "status": "completed", + "memberRate": 4.08, + "customerRate": 3.89, + "days": 4, + "amount": 3.26, + "status": "cancelled", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:06.108Z", - "updatedAt": "2021-05-09T21:33:06.108Z" + "createdAt": "2021-05-09T21:34:26.807Z", + "updatedAt": "2021-05-09T21:34:26.807Z" }, { - "id": "8eb8fc37-5ab0-4480-8806-3d3c57ab38e1", - "workPeriodId": "8c9db4fd-78ad-4e59-acba-462487b74c3a", + "id": "e8f3d379-f5a0-47f6-b37b-cae24f5909e9", + "workPeriodId": "e8346d7b-4ada-428d-a768-c2989306f63a", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 496.54, + "memberRate": 4.08, + "customerRate": 3.89, + "days": 4, + "amount": 3.26, "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:07.419Z", - "updatedAt": "2021-05-09T21:33:07.419Z" + "createdAt": "2021-05-09T21:34:22.403Z", + "updatedAt": "2021-05-09T21:34:22.403Z" } ] }, { - "id": "18881107-cc17-4087-9b2b-a74f04187f73", - "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", - "userHandle": "aaaa", + "id": "a3bbd01d-535d-4f02-8524-0d61395b84e9", + "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", + "userHandle": "sachin-wipro", "projectId": 111, - "startDate": "2021-04-11", - "endDate": "2021-04-17", - "daysWorked": 2, - "memberRate": 22.82, - "customerRate": 2.32, - "paymentStatus": "pending", + "startDate": "2020-10-25", + "endDate": "2020-10-31", + "daysWorked": 3, + "daysPaid": 3, + "paymentTotal": 9.37, + "paymentStatus": "completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:26:17.202Z", - "updatedAt": "2021-05-09T21:47:30.586Z", + "createdAt": "2021-05-09T21:25:47.824Z", + "updatedAt": "2021-05-09T21:45:48.727Z", "payments": [ { - "id": "dd8f5c08-d6a1-4fd2-b6bd-85fb6425d13d", - "workPeriodId": "18881107-cc17-4087-9b2b-a74f04187f73", + "id": "7c27346e-a23f-46f3-b4d5-88a323fd437d", + "workPeriodId": "a3bbd01d-535d-4f02-8524-0d61395b84e9", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 372.18, + "memberRate": 15.61, + "customerRate": 9.76, + "days": 3, + "amount": 9.37, "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:31:25.690Z", - "updatedAt": "2021-05-09T21:31:25.690Z" - }, - { - "id": "c456755e-0432-4656-848b-64f9c5dc8f25", - "workPeriodId": "18881107-cc17-4087-9b2b-a74f04187f73", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 32.92, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:31:27.102Z", - "updatedAt": "2021-05-09T21:31:27.102Z" + "createdAt": "2021-05-09T21:33:46.507Z", + "updatedAt": "2021-05-09T21:33:46.507Z" } ] }, { - "id": "9b455e21-e186-4622-923a-f115d23549d1", - "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", - "userHandle": "aaaa", + "id": "fa030947-2f2f-4976-82ea-0a22ee96635a", + "resourceBookingId": "c0a12936-77ef-46fa-8c75-6996339e79f6", + "userHandle": "sachin-wipro", "projectId": 111, - "startDate": "2021-04-25", - "endDate": "2021-05-01", - "daysWorked": 2, - "memberRate": 22.82, - "customerRate": 10.1, - "paymentStatus": "cancelled", + "startDate": "2020-10-11", + "endDate": "2020-10-17", + "daysWorked": 4, + "daysPaid": 3, + "paymentTotal": 6.49, + "paymentStatus": "partially-completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:26:17.161Z", - "updatedAt": "2021-05-09T21:47:19.034Z", + "createdAt": "2021-05-09T21:25:47.815Z", + "updatedAt": "2021-05-09T21:45:41.810Z", "payments": [ { - "id": "6d59a499-44e3-41e2-8368-5baee86dd8ab", - "workPeriodId": "9b455e21-e186-4622-923a-f115d23549d1", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 57.79, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:33:28.108Z", - "updatedAt": "2021-05-09T21:33:28.108Z" - }, - { - "id": "10584b23-5ab2-44e2-a927-e020c08e4f84", - "workPeriodId": "9b455e21-e186-4622-923a-f115d23549d1", + "id": "2a4d1488-d27b-4bc8-af68-1a2b893774de", + "workPeriodId": "fa030947-2f2f-4976-82ea-0a22ee96635a", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 293.79, - "status": "cancelled", + "memberRate": 10.82, + "customerRate": 30.71, + "days": 3, + "amount": 6.49, + "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:29.348Z", - "updatedAt": "2021-05-09T21:33:29.348Z" + "createdAt": "2021-05-09T21:34:31.474Z", + "updatedAt": "2021-05-09T21:34:31.474Z" } ] } ] }, { - "id": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "id": "314084fc-e854-4b62-8901-03ea9bbf2ffa", "projectId": 111, - "userId": "a2ffdeed-704d-4cf7-b70a-93fcf61de598", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", "jobId": "b28f4e67-324f-4ada-a23a-c27499053ed4", - "status": "closed", - "startDate": "2021-05-10", - "endDate": "2021-06-15", + "status": "cancelled", + "startDate": "2020-12-27", + "endDate": "2021-01-27", "memberRate": 13.23, "customerRate": 13, "rateType": "hourly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": null, - "createdAt": "2021-05-09T21:27:15.093Z", - "updatedAt": "2021-05-09T21:27:15.093Z", + "createdAt": "2021-05-09T21:25:50.450Z", + "updatedAt": "2021-05-09T21:25:50.450Z", "workPeriods": [ { - "id": "ac0ae325-8d77-4a73-bd85-5361165801cd", - "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", - "userHandle": "lakshmiaconnmgr", + "id": "32b977c9-386a-4159-a1c3-08169ee12f6e", + "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", + "userHandle": "pshah_manager", "projectId": 111, - "startDate": "2021-06-06", - "endDate": "2021-06-12", + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 3, - "memberRate": 13.74, - "customerRate": 14.57, - "paymentStatus": "pending", + "daysPaid": 2, + "paymentTotal": 5.5, + "paymentStatus": "partially-completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:27:15.951Z", - "updatedAt": "2021-05-09T21:46:12.086Z", + "createdAt": "2021-05-09T21:25:51.435Z", + "updatedAt": "2021-05-09T21:45:08.968Z", "payments": [ { - "id": "05d09e2b-02a0-4d33-b6db-0f69a98154c6", - "workPeriodId": "ac0ae325-8d77-4a73-bd85-5361165801cd", + "id": "99e4bffb-90f8-411e-9a49-fc7779bd2c07", + "workPeriodId": "32b977c9-386a-4159-a1c3-08169ee12f6e", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 374.34, + "memberRate": 13.74, + "customerRate": 22.37, + "days": 2, + "amount": 5.5, "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:53.131Z", - "updatedAt": "2021-05-09T21:33:53.131Z" + "createdAt": "2021-05-09T21:31:57.470Z", + "updatedAt": "2021-05-09T21:31:57.470Z" }, { - "id": "00640d2d-8330-445a-b022-aa687033b2b3", - "workPeriodId": "ac0ae325-8d77-4a73-bd85-5361165801cd", + "id": "fa4c7e24-470f-4aef-a269-59b7e0b2bc05", + "workPeriodId": "32b977c9-386a-4159-a1c3-08169ee12f6e", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 275.73, - "status": "completed", + "memberRate": 13.74, + "customerRate": 22.37, + "days": 3, + "amount": 8.24, + "status": "cancelled", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:54.315Z", - "updatedAt": "2021-05-09T21:33:54.315Z" + "createdAt": "2021-05-09T21:31:56.152Z", + "updatedAt": "2021-05-09T21:31:56.152Z" } ] }, { - "id": "9d914249-82f2-422e-9ba6-c281da411266", - "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", - "userHandle": "lakshmiaconnmgr", - "projectId": 111, - "startDate": "2021-05-16", - "endDate": "2021-05-22", + "id": "13f22f72-9240-43bb-ba0c-618d0b24ad8c", + "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", + "userHandle": "pshah_manager", + "projectId": 111, + "startDate": "2021-01-17", + "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": null, - "customerRate": 9.46, - "paymentStatus": "cancelled", + "daysPaid": 5, + "paymentTotal": 24.35, + "paymentStatus": "completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:27:16.017Z", - "updatedAt": "2021-05-09T21:46:21.053Z", + "createdAt": "2021-05-09T21:25:51.398Z", + "updatedAt": "2021-05-09T21:45:01.792Z", "payments": [ { - "id": "6036c7cc-b180-4f50-8163-5fd2541c66b5", - "workPeriodId": "9d914249-82f2-422e-9ba6-c281da411266", + "id": "1d2b92e8-194f-477a-97f8-e104056e6b10", + "workPeriodId": "13f22f72-9240-43bb-ba0c-618d0b24ad8c", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 494.46, + "memberRate": 24.35, + "customerRate": 4.79, + "days": 2, + "amount": 9.74, "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:40.984Z", - "updatedAt": "2021-05-09T21:33:40.984Z" - } - ] - }, - { - "id": "4ae26d58-1910-4ace-9f09-4c62c8df6031", - "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", - "userHandle": "lakshmiaconnmgr", - "projectId": 111, - "startDate": "2021-06-13", - "endDate": "2021-06-19", - "daysWorked": 3, - "memberRate": 15.61, - "customerRate": 2.1, - "paymentStatus": "partially-completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:27:15.972Z", - "updatedAt": "2021-05-09T21:45:57.739Z", - "payments": [ + "createdAt": "2021-05-09T21:31:09.459Z", + "updatedAt": "2021-05-09T21:31:09.459Z" + }, { - "id": "e623d9c7-caa9-4e58-83f1-44741b1169f8", - "workPeriodId": "4ae26d58-1910-4ace-9f09-4c62c8df6031", + "id": "381af41e-6b0a-49fe-987f-1bcb03fda571", + "workPeriodId": "13f22f72-9240-43bb-ba0c-618d0b24ad8c", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 293.79, + "memberRate": 24.35, + "customerRate": 4.79, + "days": 3, + "amount": 14.61, "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:19.371Z", - "updatedAt": "2021-05-09T21:32:19.371Z" + "createdAt": "2021-05-09T21:31:08.119Z", + "updatedAt": "2021-05-09T21:31:08.119Z" } ] }, { - "id": "94dde794-b730-4e05-8ea6-dcc5b541d43e", - "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", - "userHandle": "lakshmiaconnmgr", + "id": "0cf74043-b432-41a5-99d9-83420a6ad8ef", + "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", + "userHandle": "pshah_manager", "projectId": 111, - "startDate": "2021-05-23", - "endDate": "2021-05-29", - "daysWorked": 3, - "memberRate": 10.82, - "customerRate": 29.89, - "paymentStatus": "partially-completed", + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 2, + "daysPaid": 2, + "paymentTotal": 5.5, + "paymentStatus": "completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:27:15.991Z", - "updatedAt": "2021-05-09T21:46:02.038Z", + "createdAt": "2021-05-09T21:25:51.419Z", + "updatedAt": "2021-05-09T21:44:51.852Z", "payments": [ { - "id": "c7013bf0-17b5-4b15-826b-385fad41caf4", - "workPeriodId": "94dde794-b730-4e05-8ea6-dcc5b541d43e", + "id": "f0f85e56-6bf4-4e1f-a1ef-c31529efe4cd", + "workPeriodId": "0cf74043-b432-41a5-99d9-83420a6ad8ef", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 144.16, + "memberRate": 13.74, + "customerRate": 21.1, + "days": 1, + "amount": 2.75, "status": "cancelled", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:20.680Z", - "updatedAt": "2021-05-09T21:33:20.680Z" + "createdAt": "2021-05-09T21:30:58.017Z", + "updatedAt": "2021-05-09T21:30:58.017Z" }, { - "id": "be9706ac-c6cb-4fff-894b-8719bcf634dc", - "workPeriodId": "94dde794-b730-4e05-8ea6-dcc5b541d43e", + "id": "ef951baa-a007-48db-a658-495c6eeda9bc", + "workPeriodId": "0cf74043-b432-41a5-99d9-83420a6ad8ef", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 275.73, - "status": "cancelled", + "memberRate": 13.74, + "customerRate": 21.1, + "days": 2, + "amount": 5.5, + "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:33:21.933Z", - "updatedAt": "2021-05-09T21:33:21.933Z" + "createdAt": "2021-05-09T21:30:59.323Z", + "updatedAt": "2021-05-09T21:30:59.323Z" } ] }, { - "id": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", - "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", - "userHandle": "lakshmiaconnmgr", + "id": "e71f17b2-acb4-4c14-a492-8cab071fe2b5", + "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", + "userHandle": "pshah_manager", "projectId": 111, - "startDate": "2021-05-30", - "endDate": "2021-06-05", - "daysWorked": 3, - "memberRate": 29.32, - "customerRate": 29.89, - "paymentStatus": "pending", + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 4, + "daysPaid": 4, + "paymentTotal": 12.49, + "paymentStatus": "completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:27:16.013Z", - "updatedAt": "2021-05-09T21:45:53.796Z", + "createdAt": "2021-05-09T21:25:51.383Z", + "updatedAt": "2021-05-09T21:45:22.801Z", "payments": [ { - "id": "6e1a114f-bfac-4ab8-93d6-e47206200540", - "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 494.46, - "status": "completed", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:34:40.036Z", - "updatedAt": "2021-05-09T21:34:40.036Z" - }, - { - "id": "10fd3b3e-f5b2-42cc-91d4-54c73c003aae", - "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 477.97, - "status": "completed", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:34:37.374Z", - "updatedAt": "2021-05-09T21:34:37.374Z" - }, - { - "id": "fbc2d96f-f6c6-4a4d-b737-14a3564b7f70", - "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:34:38.700Z", - "updatedAt": "2021-05-09T21:34:38.700Z" - }, - { - "id": "fc577d14-78e8-404c-a17b-ab496e4041d8", - "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", + "id": "a376dfdc-3330-417b-916d-3a7d4f9ab384", + "workPeriodId": "e71f17b2-acb4-4c14-a492-8cab071fe2b5", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 203.74, + "memberRate": 15.61, + "customerRate": 2.54, + "days": 4, + "amount": 12.49, "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:41.520Z", - "updatedAt": "2021-05-09T21:34:41.520Z" + "createdAt": "2021-05-09T21:34:17.363Z", + "updatedAt": "2021-05-09T21:34:17.363Z" } ] }, { - "id": "6a567293-7189-4b14-82c9-57bd3d0eab20", - "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", - "userHandle": "lakshmiaconnmgr", + "id": "028287bf-6999-4fef-bdfa-1229b4e23ac1", + "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", + "userHandle": "pshah_manager", "projectId": 111, - "startDate": "2021-05-09", - "endDate": "2021-05-15", - "daysWorked": 3, - "memberRate": 24.03, - "customerRate": 24.11, - "paymentStatus": "completed", + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:27:15.959Z", - "updatedAt": "2021-05-09T21:46:07.315Z", + "createdAt": "2021-05-09T21:25:51.432Z", + "updatedAt": "2021-05-09T21:45:16.298Z", "payments": [ { - "id": "e386f5a1-2856-4cc5-8304-1598e5683a0f", - "workPeriodId": "6a567293-7189-4b14-82c9-57bd3d0eab20", + "id": "1cb5129e-8d92-4280-a946-8cb0f5757abc", + "workPeriodId": "028287bf-6999-4fef-bdfa-1229b4e23ac1", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 57.79, + "memberRate": 27.06, + "customerRate": 2.54, + "days": 2, + "amount": 10.82, "status": "cancelled", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:47.852Z", - "updatedAt": "2021-05-09T21:32:47.852Z" + "createdAt": "2021-05-09T21:30:33.549Z", + "updatedAt": "2021-05-09T21:30:33.549Z" + }, + { + "id": "b576f845-7dea-4b56-b0de-6ce15fd2c245", + "workPeriodId": "028287bf-6999-4fef-bdfa-1229b4e23ac1", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 27.06, + "customerRate": 2.54, + "days": 2, + "amount": 10.82, + "status": "cancelled", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:30:25.688Z", + "updatedAt": "2021-05-09T21:30:25.688Z" } ] } ] }, { - "id": "d6103727-6615-4168-8169-0485577bfb3f", + "id": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", "projectId": 111, - "userId": "bef43122-426b-4b2b-acdd-9b5b3bd1c0bf", - "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "userId": "dd5adacb-444d-4992-8b7b-0c349be598db", + "jobId": "b28f4e67-324f-4ada-a23a-c27499053ed4", "status": "placed", - "startDate": "2021-03-27", - "endDate": "2021-04-27", + "startDate": "2021-05-10", + "endDate": "2021-06-15", "memberRate": 13.23, "customerRate": 13, "rateType": "hourly", "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": null, - "createdAt": "2021-05-09T21:27:11.914Z", - "updatedAt": "2021-05-09T21:27:11.914Z", + "createdAt": "2021-05-09T21:26:30.065Z", + "updatedAt": "2021-05-09T21:26:30.065Z", "workPeriods": [ { - "id": "d111a56f-593d-452e-9787-551bea504c92", - "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", - "userHandle": "testcat", + "id": "28bd80ec-ef91-4516-90d6-6979e6cc341c", + "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", + "userHandle": "amy_admin", "projectId": 111, - "startDate": "2021-04-04", - "endDate": "2021-04-10", - "daysWorked": 4, - "memberRate": 23.2, - "customerRate": 30.71, - "paymentStatus": "pending", + "startDate": "2021-05-23", + "endDate": "2021-05-29", + "daysWorked": 5, + "daysPaid": 4, + "paymentTotal": 22.72, + "paymentStatus": "partially-completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:27:12.731Z", - "updatedAt": "2021-05-09T21:48:08.381Z", + "createdAt": "2021-05-09T21:26:30.824Z", + "updatedAt": "2021-05-09T21:46:34.331Z", "payments": [ { - "id": "abb79afc-a370-4625-a067-a3b57c9b4700", - "workPeriodId": "d111a56f-593d-452e-9787-551bea504c92", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, - "status": "completed", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:34:10.371Z", - "updatedAt": "2021-05-09T21:34:10.371Z" - }, - { - "id": "fa9bd31c-6c83-4ee4-9d45-a833cfe821f5", - "workPeriodId": "d111a56f-593d-452e-9787-551bea504c92", + "id": "4892cd43-a132-4ca9-a511-8d10cdc22f5d", + "workPeriodId": "28bd80ec-ef91-4516-90d6-6979e6cc341c", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 271.42, + "memberRate": 28.4, + "customerRate": 12.25, + "days": 4, + "amount": 22.72, "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:34:11.662Z", - "updatedAt": "2021-05-09T21:34:11.662Z" + "createdAt": "2021-05-09T21:31:43.808Z", + "updatedAt": "2021-05-09T21:31:43.808Z" } ] }, { - "id": "061f31fb-4f8c-462f-92c2-e5d275c45fde", - "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", - "userHandle": "testcat", + "id": "355c7114-753a-4f99-b026-1d1430bf5530", + "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", + "userHandle": "amy_admin", "projectId": 111, - "startDate": "2021-03-28", - "endDate": "2021-04-03", - "daysWorked": 2, - "memberRate": 8.09, - "customerRate": 22.15, - "paymentStatus": "pending", + "startDate": "2021-05-16", + "endDate": "2021-05-22", + "daysWorked": 5, + "daysPaid": 4, + "paymentTotal": 7.4, + "paymentStatus": "partially-completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:27:12.733Z", - "updatedAt": "2021-05-09T21:47:38.022Z", + "createdAt": "2021-05-09T21:26:30.865Z", + "updatedAt": "2021-05-09T21:46:39.040Z", "payments": [ { - "id": "3770680c-8045-43d4-8baf-cb7b3b714d39", - "workPeriodId": "061f31fb-4f8c-462f-92c2-e5d275c45fde", + "id": "71b3b7d4-129c-4348-9ead-6f22eafa6db8", + "workPeriodId": "355c7114-753a-4f99-b026-1d1430bf5530", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 477.97, - "status": "cancelled", + "memberRate": 9.26, + "customerRate": 16.44, + "days": 2, + "amount": 3.7, + "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:42.711Z", - "updatedAt": "2021-05-09T21:30:42.711Z" + "createdAt": "2021-05-09T21:32:05.827Z", + "updatedAt": "2021-05-09T21:32:05.827Z" }, { - "id": "c658d66e-86e1-49c7-8051-2b9a017935ad", - "workPeriodId": "061f31fb-4f8c-462f-92c2-e5d275c45fde", + "id": "d0ebcc96-70f2-4716-92d4-74e40af04387", + "workPeriodId": "355c7114-753a-4f99-b026-1d1430bf5530", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 248.38, + "memberRate": 9.26, + "customerRate": 16.44, + "days": 2, + "amount": 3.7, "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:41.304Z", - "updatedAt": "2021-05-09T21:30:41.304Z" + "createdAt": "2021-05-09T21:32:04.463Z", + "updatedAt": "2021-05-09T21:32:04.463Z" } ] }, { - "id": "5904b1d9-cb50-4b5d-8103-6741fec2f86b", - "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", - "userHandle": "testcat", + "id": "9c024b6a-229a-4b8d-ac03-a53e4e412a73", + "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", + "userHandle": "amy_admin", "projectId": 111, - "startDate": "2021-04-18", - "endDate": "2021-04-24", - "daysWorked": 2, - "memberRate": 22.82, - "customerRate": 4.07, - "paymentStatus": "cancelled", + "startDate": "2021-05-09", + "endDate": "2021-05-15", + "daysWorked": 5, + "daysPaid": 5, + "paymentTotal": 5.59, + "paymentStatus": "completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:27:12.746Z", - "updatedAt": "2021-05-09T21:47:44.291Z", + "createdAt": "2021-05-09T21:26:30.813Z", + "updatedAt": "2021-05-09T21:46:53.821Z", "payments": [ { - "id": "dfc9bed6-78f2-407e-a7e4-abea9a3d3b46", - "workPeriodId": "5904b1d9-cb50-4b5d-8103-6741fec2f86b", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 48.51, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:32:27.398Z", - "updatedAt": "2021-05-09T21:32:27.398Z" - }, - { - "id": "be4d5099-8b8e-45e2-b6cd-ab1997f57e26", - "workPeriodId": "5904b1d9-cb50-4b5d-8103-6741fec2f86b", + "id": "28f285ec-0965-4904-901d-7ae5c5d8e220", + "workPeriodId": "9c024b6a-229a-4b8d-ac03-a53e4e412a73", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 477.97, + "memberRate": 5.59, + "customerRate": 18.55, + "days": 5, + "amount": 5.59, "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:26.174Z", - "updatedAt": "2021-05-09T21:32:26.174Z" + "createdAt": "2021-05-09T21:33:35.441Z", + "updatedAt": "2021-05-09T21:33:35.441Z" } ] }, { - "id": "6ed56eb5-cadc-45f8-bbdf-3f408948c274", - "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", - "userHandle": "testcat", + "id": "48c4b614-6588-46c9-8b4f-54d3175ae47d", + "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", + "userHandle": "amy_admin", "projectId": 111, - "startDate": "2021-03-21", - "endDate": "2021-03-27", - "daysWorked": 4, - "memberRate": 22.85, - "customerRate": 29.89, + "startDate": "2021-06-06", + "endDate": "2021-06-12", + "daysWorked": 5, + "daysPaid": 5, + "paymentTotal": 16.02, "paymentStatus": "completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:27:12.737Z", - "updatedAt": "2021-05-09T21:47:58.216Z", + "createdAt": "2021-05-09T21:26:30.878Z", + "updatedAt": "2021-05-09T21:46:27.840Z", "payments": [ { - "id": "ea3bdc7a-7c14-4ac1-955d-2540589fcfa6", - "workPeriodId": "6ed56eb5-cadc-45f8-bbdf-3f408948c274", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:33:00.899Z", - "updatedAt": "2021-05-09T21:33:00.899Z" - }, - { - "id": "3bf29da2-581c-4f9c-8b0f-ff3c876848a0", - "workPeriodId": "6ed56eb5-cadc-45f8-bbdf-3f408948c274", + "id": "e1f9634a-4c3b-4dcd-ad81-3703c807a820", + "workPeriodId": "48c4b614-6588-46c9-8b4f-54d3175ae47d", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 293.79, - "status": "cancelled", + "memberRate": 16.02, + "customerRate": 30.71, + "days": 5, + "amount": 16.02, + "status": "completed", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:59.500Z", - "updatedAt": "2021-05-09T21:32:59.500Z" + "createdAt": "2021-05-09T21:32:12.499Z", + "updatedAt": "2021-05-09T21:32:12.499Z" } ] }, { - "id": "6adbf80c-43aa-41ad-b7db-544dc76f9b1c", - "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", - "userHandle": "testcat", + "id": "91fcf91f-b2cf-4909-8f03-b5efc0732b28", + "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", + "userHandle": "amy_admin", "projectId": 111, - "startDate": "2021-04-25", - "endDate": "2021-05-01", - "daysWorked": 0, - "memberRate": 29.32, - "customerRate": 22.15, - "paymentStatus": "completed", + "startDate": "2021-06-13", + "endDate": "2021-06-19", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:27:12.794Z", - "updatedAt": "2021-05-09T21:47:51.348Z", + "createdAt": "2021-05-09T21:26:30.893Z", + "updatedAt": "2021-05-09T21:46:44.896Z", "payments": [ { - "id": "e85008c3-feff-40ab-bf4c-bc80f0bb5288", - "workPeriodId": "6adbf80c-43aa-41ad-b7db-544dc76f9b1c", + "id": "d36afd1d-1f76-4f2d-b630-69bf85796496", + "workPeriodId": "91fcf91f-b2cf-4909-8f03-b5efc0732b28", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 1.53, + "customerRate": 2.1, + "days": 5, + "amount": 1.53, + "status": "cancelled", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:12.838Z", + "updatedAt": "2021-05-09T21:33:12.838Z" + }, + { + "id": "71750282-0ffe-46ed-b8c2-37e36c148833", + "workPeriodId": "91fcf91f-b2cf-4909-8f03-b5efc0732b28", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 460.88, + "memberRate": 1.53, + "customerRate": 2.1, + "days": 3, + "amount": 0.92, "status": "cancelled", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:32:53.856Z", - "updatedAt": "2021-05-09T21:32:53.856Z" + "createdAt": "2021-05-09T21:33:14.202Z", + "updatedAt": "2021-05-09T21:33:14.202Z" } ] }, { - "id": "0c825dec-6e7b-4dde-943a-f3f8354219cc", - "resourceBookingId": "d6103727-6615-4168-8169-0485577bfb3f", - "userHandle": "testcat", + "id": "311a692f-18bc-4eb6-a6d0-06cf3dbd1637", + "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", + "userHandle": "amy_admin", "projectId": 111, - "startDate": "2021-04-11", - "endDate": "2021-04-17", - "daysWorked": 4, - "memberRate": 5.59, - "customerRate": 30.54, - "paymentStatus": "partially-completed", + "startDate": "2021-05-30", + "endDate": "2021-06-05", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:27:12.783Z", - "updatedAt": "2021-05-09T21:48:03.740Z", + "createdAt": "2021-05-09T21:26:30.875Z", + "updatedAt": "2021-05-09T21:46:49.092Z", "payments": [ { - "id": "78641310-ad51-40a3-a0fd-fdd8d15455b9", - "workPeriodId": "0c825dec-6e7b-4dde-943a-f3f8354219cc", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 417.42, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:30:50.460Z", - "updatedAt": "2021-05-09T21:30:50.460Z" - }, - { - "id": "5bdd5c22-d9b7-428c-b084-d0950a18bc37", - "workPeriodId": "0c825dec-6e7b-4dde-943a-f3f8354219cc", + "id": "875c7250-50fa-453f-99d0-e76260648cd1", + "workPeriodId": "311a692f-18bc-4eb6-a6d0-06cf3dbd1637", "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 57.79, + "memberRate": 13.07, + "customerRate": 10.1, + "days": 5, + "amount": 13.07, "status": "cancelled", + "statusDetails": null, "billingAccountId": 80000071, "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": null, - "createdAt": "2021-05-09T21:30:49.123Z", - "updatedAt": "2021-05-09T21:30:49.123Z" + "createdAt": "2021-05-09T21:31:50.023Z", + "updatedAt": "2021-05-09T21:31:50.023Z" } ] } ] }, { - "id": "314084fc-e854-4b62-8901-03ea9bbf2ffa", - "projectId": 111, - "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", - "jobId": "b28f4e67-324f-4ada-a23a-c27499053ed4", - "status": "cancelled", - "startDate": "2020-12-27", - "endDate": "2021-01-27", - "memberRate": 13.23, - "customerRate": 13, - "rateType": "hourly", + "id": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "projectId": 16870, + "userId": "acdf9ebe-8358-4bd3-9374-1d86cf27e5f4", + "jobId": "fe8da845-5313-496f-b859-9824bd06a0db", + "status": "placed", + "startDate": "2021-01-21", + "endDate": "2021-02-21", + "memberRate": 114.33, + "customerRate": 258.37, + "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-05-09T21:25:50.450Z", - "updatedAt": "2021-05-09T21:25:50.450Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T09:30:59.306Z", + "updatedAt": "2021-05-30T11:48:56.459Z", "workPeriods": [ { - "id": "13f22f72-9240-43bb-ba0c-618d0b24ad8c", - "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", - "userHandle": "pshah_manager", - "projectId": 111, + "id": "97cb1bad-772a-4f1e-a5a3-f0b19ae766f2", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-02-21", + "endDate": "2021-02-27", + "daysWorked": 0, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "noDays", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:57.210Z", + "updatedAt": "2021-05-30T16:12:01.358Z", + "payments": [] + }, + { + "id": "55b66454-3a29-4163-9d97-7ecd2e805f71", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, "startDate": "2021-01-17", "endDate": "2021-01-23", - "daysWorked": 5, - "memberRate": 24.35, - "customerRate": 4.79, + "daysWorked": 2, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:25:51.398Z", - "updatedAt": "2021-05-09T21:45:01.792Z", - "payments": [ - { - "id": "1d2b92e8-194f-477a-97f8-e104056e6b10", - "workPeriodId": "13f22f72-9240-43bb-ba0c-618d0b24ad8c", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 39.66, - "status": "completed", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:31:09.459Z", - "updatedAt": "2021-05-09T21:31:09.459Z" - }, - { - "id": "381af41e-6b0a-49fe-987f-1bcb03fda571", - "workPeriodId": "13f22f72-9240-43bb-ba0c-618d0b24ad8c", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 460.88, - "status": "completed", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:31:08.119Z", - "updatedAt": "2021-05-09T21:31:08.119Z" - } - ] + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:57.225Z", + "updatedAt": "2021-05-30T16:12:01.359Z", + "payments": [] }, { - "id": "0cf74043-b432-41a5-99d9-83420a6ad8ef", - "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", - "userHandle": "pshah_manager", - "projectId": 111, - "startDate": "2021-01-03", - "endDate": "2021-01-09", - "daysWorked": 2, - "memberRate": 13.74, - "customerRate": 21.1, - "paymentStatus": "cancelled", + "id": "5bc5686b-95b3-49d7-9c8e-50f1dfdcb82e", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:25:51.419Z", - "updatedAt": "2021-05-09T21:44:51.852Z", - "payments": [ - { - "id": "f0f85e56-6bf4-4e1f-a1ef-c31529efe4cd", - "workPeriodId": "0cf74043-b432-41a5-99d9-83420a6ad8ef", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 466.42, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:30:58.017Z", - "updatedAt": "2021-05-09T21:30:58.017Z" - }, - { - "id": "ef951baa-a007-48db-a658-495c6eeda9bc", - "workPeriodId": "0cf74043-b432-41a5-99d9-83420a6ad8ef", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 448.51, - "status": "completed", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:30:59.323Z", - "updatedAt": "2021-05-09T21:30:59.323Z" - } - ] + "createdAt": "2021-05-30T11:48:57.306Z", + "updatedAt": "2021-05-30T16:04:57.957Z", + "payments": [] }, { - "id": "028287bf-6999-4fef-bdfa-1229b4e23ac1", - "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", - "userHandle": "pshah_manager", - "projectId": 111, - "startDate": "2021-01-24", - "endDate": "2021-01-30", - "daysWorked": 4, - "memberRate": 27.06, - "customerRate": 2.54, + "id": "77dcf745-bd59-40d8-b563-75a4d5354d29", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-02-14", + "endDate": "2021-02-20", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:25:51.432Z", - "updatedAt": "2021-05-09T21:45:16.298Z", - "payments": [ - { - "id": "b576f845-7dea-4b56-b0de-6ce15fd2c245", - "workPeriodId": "028287bf-6999-4fef-bdfa-1229b4e23ac1", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 477.97, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:30:25.688Z", - "updatedAt": "2021-05-09T21:30:25.688Z" - }, - { - "id": "1cb5129e-8d92-4280-a946-8cb0f5757abc", - "workPeriodId": "028287bf-6999-4fef-bdfa-1229b4e23ac1", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 460.88, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:30:33.549Z", - "updatedAt": "2021-05-09T21:30:33.549Z" - } - ] + "createdAt": "2021-05-30T11:48:57.208Z", + "updatedAt": "2021-05-30T16:04:30.745Z", + "payments": [] }, { - "id": "e71f17b2-acb4-4c14-a492-8cab071fe2b5", - "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", - "userHandle": "pshah_manager", - "projectId": 111, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 4, - "memberRate": 15.61, - "customerRate": 2.54, + "id": "1fa1f111-6574-47b0-8d12-6832541d496c", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:25:51.383Z", - "updatedAt": "2021-05-09T21:45:22.801Z", - "payments": [ - { - "id": "a376dfdc-3330-417b-916d-3a7d4f9ab384", - "workPeriodId": "e71f17b2-acb4-4c14-a492-8cab071fe2b5", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 466.42, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:34:17.363Z", - "updatedAt": "2021-05-09T21:34:17.363Z" - } - ] + "createdAt": "2021-05-30T11:48:57.244Z", + "updatedAt": "2021-05-30T16:05:42.410Z", + "payments": [] }, { - "id": "32b977c9-386a-4159-a1c3-08169ee12f6e", - "resourceBookingId": "314084fc-e854-4b62-8901-03ea9bbf2ffa", - "userHandle": "pshah_manager", - "projectId": 111, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": 3, - "memberRate": 13.74, - "customerRate": 22.37, - "paymentStatus": "cancelled", + "id": "4c626a59-e591-4e7a-88cb-1d601b9b8493", + "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", + "userHandle": "newwayenjoy", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:25:51.435Z", - "updatedAt": "2021-05-09T21:45:08.968Z", - "payments": [ - { - "id": "99e4bffb-90f8-411e-9a49-fc7779bd2c07", - "workPeriodId": "32b977c9-386a-4159-a1c3-08169ee12f6e", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 416.38, - "status": "completed", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:31:57.470Z", - "updatedAt": "2021-05-09T21:31:57.470Z" - }, - { - "id": "fa4c7e24-470f-4aef-a269-59b7e0b2bc05", - "workPeriodId": "32b977c9-386a-4159-a1c3-08169ee12f6e", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 460.88, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:31:56.152Z", - "updatedAt": "2021-05-09T21:31:56.152Z" - } - ] + "createdAt": "2021-05-30T11:48:57.302Z", + "updatedAt": "2021-05-30T16:05:10.190Z", + "payments": [] } ] }, { - "id": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", - "projectId": 111, - "userId": "dd5adacb-444d-4992-8b7b-0c349be598db", - "jobId": "b28f4e67-324f-4ada-a23a-c27499053ed4", - "status": "placed", - "startDate": "2021-05-10", - "endDate": "2021-06-15", - "memberRate": 13.23, - "customerRate": 13, - "rateType": "hourly", + "id": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "projectId": 17103, + "userId": "8fe0c1c3-e63e-4047-9854-01f03b166bd8", + "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "status": "closed", + "startDate": "2021-01-02", + "endDate": "2021-02-02", + "memberRate": 85.22, + "customerRate": 170.64, + "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": null, - "createdAt": "2021-05-09T21:26:30.065Z", - "updatedAt": "2021-05-09T21:26:30.065Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-01T10:25:45.827Z", + "updatedAt": "2021-05-30T11:49:17.657Z", "workPeriods": [ { - "id": "28bd80ec-ef91-4516-90d6-6979e6cc341c", - "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-05-23", - "endDate": "2021-05-29", - "daysWorked": 5, - "memberRate": 28.4, - "customerRate": 12.25, - "paymentStatus": "partially-completed", + "id": "0466ddf6-83ba-41ee-b299-4abb2b5f8a3b", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 0, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "noDays", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:26:30.824Z", - "updatedAt": "2021-05-09T21:46:34.331Z", - "payments": [ - { - "id": "4892cd43-a132-4ca9-a511-8d10cdc22f5d", - "workPeriodId": "28bd80ec-ef91-4516-90d6-6979e6cc341c", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 374.34, - "status": "completed", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:31:43.808Z", - "updatedAt": "2021-05-09T21:31:43.808Z" - } - ] + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:18.405Z", + "updatedAt": "2021-05-30T16:12:15.134Z", + "payments": [] }, { - "id": "355c7114-753a-4f99-b026-1d1430bf5530", - "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-05-16", - "endDate": "2021-05-22", + "id": "bd92f07b-4b57-4486-9101-254578cf32f8", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-17", + "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 9.26, - "customerRate": 16.44, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:26:30.865Z", - "updatedAt": "2021-05-09T21:46:39.040Z", - "payments": [ - { - "id": "71b3b7d4-129c-4348-9ead-6f22eafa6db8", - "workPeriodId": "355c7114-753a-4f99-b026-1d1430bf5530", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 416.38, - "status": "completed", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:32:05.827Z", - "updatedAt": "2021-05-09T21:32:05.827Z" - }, - { - "id": "d0ebcc96-70f2-4716-92d4-74e40af04387", - "workPeriodId": "355c7114-753a-4f99-b026-1d1430bf5530", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 416.38, - "status": "completed", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:32:04.463Z", - "updatedAt": "2021-05-09T21:32:04.463Z" - } - ] + "createdAt": "2021-05-30T11:49:18.505Z", + "updatedAt": "2021-05-30T16:03:42.520Z", + "payments": [] }, { - "id": "9c024b6a-229a-4b8d-ac03-a53e4e412a73", - "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-05-09", - "endDate": "2021-05-15", + "id": "9c976d1a-f395-4889-ac9b-38846a083dcb", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-24", + "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 5.59, - "customerRate": 18.55, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:26:30.813Z", - "updatedAt": "2021-05-09T21:46:53.821Z", - "payments": [ - { - "id": "28f285ec-0965-4904-901d-7ae5c5d8e220", - "workPeriodId": "9c024b6a-229a-4b8d-ac03-a53e4e412a73", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 496.54, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:33:35.441Z", - "updatedAt": "2021-05-09T21:33:35.441Z" - } - ] + "createdAt": "2021-05-30T11:49:18.500Z", + "updatedAt": "2021-05-30T16:04:11.932Z", + "payments": [] }, { - "id": "48c4b614-6588-46c9-8b4f-54d3175ae47d", - "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-06-06", - "endDate": "2021-06-12", + "id": "62bf7ac9-bea9-4f96-8a28-2a3a8dbbc48f", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-03", + "endDate": "2021-01-09", "daysWorked": 5, - "memberRate": 16.02, - "customerRate": 30.71, - "paymentStatus": "cancelled", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:26:30.878Z", - "updatedAt": "2021-05-09T21:46:27.840Z", - "payments": [ - { - "id": "e1f9634a-4c3b-4dcd-ad81-3703c807a820", - "workPeriodId": "48c4b614-6588-46c9-8b4f-54d3175ae47d", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 293.79, - "status": "completed", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:32:12.499Z", - "updatedAt": "2021-05-09T21:32:12.499Z" - } - ] + "createdAt": "2021-05-30T11:49:18.439Z", + "updatedAt": "2021-05-30T16:04:50.801Z", + "payments": [] }, { - "id": "91fcf91f-b2cf-4909-8f03-b5efc0732b28", - "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-06-13", - "endDate": "2021-06-19", - "daysWorked": 5, - "memberRate": 1.53, - "customerRate": 2.1, - "paymentStatus": "completed", + "id": "05fb419d-927c-4264-b346-905ba7a55f49", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 2, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:26:30.893Z", - "updatedAt": "2021-05-09T21:46:44.896Z", - "payments": [ - { - "id": "71750282-0ffe-46ed-b8c2-37e36c148833", - "workPeriodId": "91fcf91f-b2cf-4909-8f03-b5efc0732b28", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 203.74, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:33:14.202Z", - "updatedAt": "2021-05-09T21:33:14.202Z" - }, - { - "id": "d36afd1d-1f76-4f2d-b630-69bf85796496", - "workPeriodId": "91fcf91f-b2cf-4909-8f03-b5efc0732b28", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 494.46, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:33:12.838Z", - "updatedAt": "2021-05-09T21:33:12.838Z" - } - ] + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:18.492Z", + "updatedAt": "2021-05-30T16:12:15.133Z", + "payments": [] }, { - "id": "311a692f-18bc-4eb6-a6d0-06cf3dbd1637", - "resourceBookingId": "d4134cc8-d306-45e2-bb76-cf3707b6df3e", - "userHandle": "amy_admin", - "projectId": 111, - "startDate": "2021-05-30", - "endDate": "2021-06-05", - "daysWorked": 5, - "memberRate": 13.07, - "customerRate": 10.1, - "paymentStatus": "completed", + "id": "c4b535c4-0c6f-4420-930e-0103aea68057", + "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", + "userHandle": "marathon_zhang", + "projectId": 17103, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-09T21:26:30.875Z", - "updatedAt": "2021-05-09T21:46:49.092Z", - "payments": [ - { - "id": "875c7250-50fa-453f-99d0-e76260648cd1", - "workPeriodId": "311a692f-18bc-4eb6-a6d0-06cf3dbd1637", - "challengeId": "00000000-0000-0000-0000-000000000000", - "amount": 248.38, - "status": "cancelled", - "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "updatedBy": null, - "createdAt": "2021-05-09T21:31:50.023Z", - "updatedAt": "2021-05-09T21:31:50.023Z" - } - ] + "createdAt": "2021-05-30T11:49:18.490Z", + "updatedAt": "2021-05-30T16:03:36.317Z", + "payments": [] } ] }, @@ -2680,37 +2663,20 @@ "updatedAt": "2021-05-30T11:41:11.436Z", "workPeriods": [ { - "id": "f5ab8f98-7e4b-4cf9-a1e5-6a15ab6bad91", - "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", - "userHandle": "nkumar2", - "projectId": 16714, - "startDate": "2021-01-03", - "endDate": "2021-01-09", - "daysWorked": 5, - "memberRate": 63.94, - "customerRate": 16.07, - "paymentStatus": "completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:41:12.251Z", - "updatedAt": "2021-05-30T16:02:51.923Z", - "payments": [] - }, - { - "id": "c20620e6-17de-41f5-8a0a-089683550b2f", + "id": "15cbf62c-67d9-4f4a-92dc-546cb0afac32", "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", "userHandle": "nkumar2", "projectId": 16714, - "startDate": "2021-01-17", - "endDate": "2021-01-23", - "daysWorked": 5, - "memberRate": 248.87, - "customerRate": 217.32, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:41:12.260Z", - "updatedAt": "2021-05-30T16:03:39.019Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:41:12.216Z", + "updatedAt": "2021-05-30T16:12:06.801Z", "payments": [] }, { @@ -2721,9 +2687,9 @@ "startDate": "2021-01-10", "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 248.87, - "customerRate": 281.39, - "paymentStatus": "completed", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "createdAt": "2021-05-30T11:41:12.223Z", @@ -2738,8 +2704,8 @@ "startDate": "2021-01-31", "endDate": "2021-02-06", "daysWorked": 1, - "memberRate": 196.23, - "customerRate": 271.77, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", @@ -2755,8 +2721,8 @@ "startDate": "2021-01-24", "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 63.04, - "customerRate": 47.87, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -2765,20 +2731,37 @@ "payments": [] }, { - "id": "15cbf62c-67d9-4f4a-92dc-546cb0afac32", + "id": "f5ab8f98-7e4b-4cf9-a1e5-6a15ab6bad91", "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", "userHandle": "nkumar2", "projectId": 16714, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 1, - "memberRate": 295.62, - "customerRate": 235.7, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:41:12.216Z", - "updatedAt": "2021-05-30T16:12:06.801Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:12.251Z", + "updatedAt": "2021-05-30T16:02:51.923Z", + "payments": [] + }, + { + "id": "c20620e6-17de-41f5-8a0a-089683550b2f", + "resourceBookingId": "3f12b87c-9915-4ce1-89e5-535a3c0337f4", + "userHandle": "nkumar2", + "projectId": 16714, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:41:12.260Z", + "updatedAt": "2021-05-30T16:03:39.019Z", "payments": [] } ] @@ -2808,8 +2791,8 @@ "startDate": "2020-12-27", "endDate": "2021-01-02", "daysWorked": 1, - "memberRate": 8.1, - "customerRate": 155.91, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", @@ -2825,8 +2808,8 @@ "startDate": "2021-01-17", "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 216.18, - "customerRate": 262.91, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -2834,23 +2817,6 @@ "updatedAt": "2021-05-30T16:03:44.298Z", "payments": [] }, - { - "id": "31436a53-4c2e-40fc-98f2-2b6e3e36aa2b", - "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", - "userHandle": "jimsun", - "projectId": 17091, - "startDate": "2021-01-31", - "endDate": "2021-02-06", - "daysWorked": null, - "memberRate": 104.85, - "customerRate": 225.57, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:18.565Z", - "updatedAt": "2021-05-30T16:11:38.208Z", - "payments": [] - }, { "id": "b04da80e-eff3-4be3-8849-f39f6af417b9", "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", @@ -2859,8 +2825,8 @@ "startDate": "2021-01-03", "endDate": "2021-01-09", "daysWorked": 5, - "memberRate": 131.12, - "customerRate": 270.74, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -2876,8 +2842,8 @@ "startDate": "2021-01-10", "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 34.56, - "customerRate": 265.36, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -2893,50 +2859,188 @@ "startDate": "2021-01-24", "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 30.95, - "customerRate": 54.36, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "createdAt": "2021-05-30T11:48:18.552Z", "updatedAt": "2021-05-30T16:03:06.211Z", "payments": [] + }, + { + "id": "31436a53-4c2e-40fc-98f2-2b6e3e36aa2b", + "resourceBookingId": "e8e5ba0d-d506-4f76-b920-e6efcee29611", + "userHandle": "jimsun", + "projectId": 17091, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:18.565Z", + "updatedAt": "2021-05-30T16:11:38.208Z", + "payments": [] } ] }, { - "id": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", - "projectId": 16781, - "userId": "7eea7c2f-5a46-4646-82bd-db4ac528378d", - "jobId": "ff3feeae-d4f7-457c-bff7-215be5efe2b8", - "status": "sourcing", - "startDate": "2021-01-11", - "endDate": "2021-02-11", - "memberRate": 61.33, - "customerRate": 196.21, + "id": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "projectId": 16870, + "userId": "2bba34d5-20e4-46d6-bfc1-05736b17afbb", + "jobId": "fe539bef-9119-4a8c-b7b0-915e7e3a3ba3", + "status": "closed", + "startDate": "2021-01-02", + "endDate": "2021-02-02", + "memberRate": 18.4, + "customerRate": 30.14, "rateType": "weekly", "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-12T10:49:14.209Z", - "updatedAt": "2021-05-30T11:48:22.088Z", + "createdAt": "2021-05-01T03:43:44.374Z", + "updatedAt": "2021-05-30T11:49:00.956Z", "workPeriods": [ { - "id": "41217a15-4231-480c-91bc-492cbbe95113", - "resourceBookingId": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", - "userHandle": "ZeroChance", - "projectId": 16781, - "startDate": "2021-02-07", - "endDate": "2021-02-13", - "daysWorked": 4, - "memberRate": 217.78, - "customerRate": 134.84, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:22.909Z", - "updatedAt": "2021-05-30T16:11:40.871Z", - "payments": [] + "id": "b9768ae4-ae40-4bb9-8bc2-970c9f36f0bf", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 0, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "noDays", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:01.820Z", + "updatedAt": "2021-05-30T16:12:04.121Z", + "payments": [] + }, + { + "id": "77ac0f07-d4ae-463e-a9db-437623a29958", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:01.793Z", + "updatedAt": "2021-05-30T16:04:31.663Z", + "payments": [] + }, + { + "id": "1f5d3abe-fe2b-4961-831c-b7bbdf76da82", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:01.732Z", + "updatedAt": "2021-05-30T16:05:43.321Z", + "payments": [] + }, + { + "id": "1c526f5f-d9e0-4e16-8421-cee4e8154a3c", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:01.802Z", + "updatedAt": "2021-05-30T16:05:48.592Z", + "payments": [] + }, + { + "id": "b7bce7db-65dc-4447-8865-f6e8a84a867e", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:01.698Z", + "updatedAt": "2021-05-30T16:03:48.778Z", + "payments": [] + }, + { + "id": "a475c2fa-c5ce-4a30-bdd8-d4fad1e6308c", + "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 2, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:01.795Z", + "updatedAt": "2021-05-30T16:12:04.120Z", + "payments": [] + } + ] + }, + { + "id": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", + "projectId": 16781, + "userId": "7eea7c2f-5a46-4646-82bd-db4ac528378d", + "jobId": "ff3feeae-d4f7-457c-bff7-215be5efe2b8", + "status": "sourcing", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 61.33, + "customerRate": 196.21, + "rateType": "weekly", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-01-12T10:49:14.209Z", + "updatedAt": "2021-05-30T11:48:22.088Z", + "workPeriods": [ + { + "id": "41217a15-4231-480c-91bc-492cbbe95113", + "resourceBookingId": "de7e5be7-c8a9-416b-b8d0-d3faff274a40", + "userHandle": "ZeroChance", + "projectId": 16781, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:22.909Z", + "updatedAt": "2021-05-30T16:11:40.871Z", + "payments": [] }, { "id": "cd2ff33c-70d9-4b47-a1f2-d3a32febb22d", @@ -2946,8 +3050,8 @@ "startDate": "2021-01-24", "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 297.89, - "customerRate": 277.1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -2963,8 +3067,8 @@ "startDate": "2021-01-31", "endDate": "2021-02-06", "daysWorked": 4, - "memberRate": 197.07, - "customerRate": 93.25, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -2980,8 +3084,8 @@ "startDate": "2021-01-17", "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 115.09, - "customerRate": 282.56, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -2997,9 +3101,9 @@ "startDate": "2021-01-10", "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 175.62, - "customerRate": 101, - "paymentStatus": "completed", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "createdAt": "2021-05-30T11:48:22.912Z", @@ -3033,8 +3137,8 @@ "startDate": "2021-01-31", "endDate": "2021-02-06", "daysWorked": 5, - "memberRate": 178.01, - "customerRate": 282.9, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -3050,8 +3154,8 @@ "startDate": "2021-01-24", "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 148.65, - "customerRate": 131.63, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -3067,8 +3171,8 @@ "startDate": "2021-01-10", "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 69.98, - "customerRate": 245.82, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -3084,8 +3188,8 @@ "startDate": "2021-02-07", "endDate": "2021-02-13", "daysWorked": 4, - "memberRate": 28.27, - "customerRate": 154.51, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", @@ -3101,8 +3205,8 @@ "startDate": "2021-01-17", "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 118.86, - "customerRate": 300.07, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -3113,3722 +3217,4085 @@ ] }, { - "id": "f40f43a8-b7b0-4181-967c-26e4e070f95e", - "projectId": 16870, - "userId": "b851684b-1071-47c3-8719-bdae96aa0e6d", - "jobId": "fed687e1-4257-48bb-806c-38712f9bf14f", - "status": "placed", - "startDate": "2021-01-11", - "endDate": "2021-02-11", - "memberRate": 115.29, - "customerRate": 278.45, - "rateType": "weekly", + "id": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "projectId": 111, + "userId": "a2ffdeed-704d-4cf7-b70a-93fcf61de598", + "jobId": "b28f4e67-324f-4ada-a23a-c27499053ed4", + "status": "closed", + "startDate": "2021-05-10", + "endDate": "2021-06-15", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-04-29T09:50:13.905Z", - "updatedAt": "2021-05-30T11:48:15.196Z", + "updatedBy": null, + "createdAt": "2021-05-09T21:27:15.093Z", + "updatedAt": "2021-05-09T21:27:15.093Z", "workPeriods": [ { - "id": "e95f0541-bc37-4c1e-a619-029753c1a69a", - "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", - "userHandle": "zxx.lotus", - "projectId": 16870, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": 5, - "memberRate": 43.3, - "customerRate": 40.52, - "paymentStatus": "cancelled", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:16.045Z", - "updatedAt": "2021-05-30T16:03:08.035Z", - "payments": [] - }, - { - "id": "cb6114ca-1624-4239-8243-aee05f8b5fe5", - "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", - "userHandle": "zxx.lotus", - "projectId": 16870, - "startDate": "2021-01-31", - "endDate": "2021-02-06", - "daysWorked": 5, - "memberRate": 216.67, - "customerRate": 260.18, + "id": "94dde794-b730-4e05-8ea6-dcc5b541d43e", + "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "userHandle": "lakshmiaconnmgr", + "projectId": 111, + "startDate": "2021-05-23", + "endDate": "2021-05-29", + "daysWorked": 3, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:16.042Z", - "updatedAt": "2021-05-30T16:03:29.374Z", - "payments": [] + "createdAt": "2021-05-09T21:27:15.991Z", + "updatedAt": "2021-05-09T21:46:02.038Z", + "payments": [ + { + "id": "c7013bf0-17b5-4b15-826b-385fad41caf4", + "workPeriodId": "94dde794-b730-4e05-8ea6-dcc5b541d43e", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 10.82, + "customerRate": 29.89, + "days": 3, + "amount": 6.49, + "status": "cancelled", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:20.680Z", + "updatedAt": "2021-05-09T21:33:20.680Z" + }, + { + "id": "be9706ac-c6cb-4fff-894b-8719bcf634dc", + "workPeriodId": "94dde794-b730-4e05-8ea6-dcc5b541d43e", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 10.82, + "customerRate": 29.89, + "days": 2, + "amount": 4.33, + "status": "cancelled", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:21.933Z", + "updatedAt": "2021-05-09T21:33:21.933Z" + } + ] }, { - "id": "f382c2c8-fe32-42f3-b960-29c345ab0264", - "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", - "userHandle": "zxx.lotus", - "projectId": 16870, - "startDate": "2021-02-07", - "endDate": "2021-02-13", - "daysWorked": 4, - "memberRate": 185.3, - "customerRate": 203.04, + "id": "6a567293-7189-4b14-82c9-57bd3d0eab20", + "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "userHandle": "lakshmiaconnmgr", + "projectId": 111, + "startDate": "2021-05-09", + "endDate": "2021-05-15", + "daysWorked": 3, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:16.004Z", - "updatedAt": "2021-05-30T16:11:36.370Z", - "payments": [] + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:15.959Z", + "updatedAt": "2021-05-09T21:46:07.315Z", + "payments": [ + { + "id": "e386f5a1-2856-4cc5-8304-1598e5683a0f", + "workPeriodId": "6a567293-7189-4b14-82c9-57bd3d0eab20", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 24.03, + "customerRate": 24.11, + "days": 3, + "amount": 14.42, + "status": "cancelled", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:47.852Z", + "updatedAt": "2021-05-09T21:32:47.852Z" + } + ] }, { - "id": "9215b576-8f67-4511-88f9-76000d6c8326", - "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", - "userHandle": "zxx.lotus", - "projectId": 16870, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", + "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "userHandle": "lakshmiaconnmgr", + "projectId": 111, + "startDate": "2021-05-30", + "endDate": "2021-06-05", "daysWorked": 5, - "memberRate": 283.73, - "customerRate": 232.51, - "paymentStatus": "pending", + "daysPaid": 4, + "paymentTotal": 23.45, + "paymentStatus": "partially-completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:16.008Z", - "updatedAt": "2021-05-30T16:04:19.062Z", - "payments": [] + "createdAt": "2021-05-09T21:27:16.013Z", + "updatedAt": "2021-05-09T21:45:53.796Z", + "payments": [ + { + "id": "6e1a114f-bfac-4ab8-93d6-e47206200540", + "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 29.32, + "customerRate": 29.89, + "days": 1, + "amount": 5.86, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:40.036Z", + "updatedAt": "2021-05-09T21:34:40.036Z" + }, + { + "id": "10fd3b3e-f5b2-42cc-91d4-54c73c003aae", + "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 29.32, + "customerRate": 29.89, + "days": 1, + "amount": 5.86, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:37.374Z", + "updatedAt": "2021-05-09T21:34:37.374Z" + }, + { + "id": "fbc2d96f-f6c6-4a4d-b737-14a3564b7f70", + "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 29.32, + "customerRate": 29.89, + "days": 1, + "amount": 5.86, + "status": "cancelled", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:38.700Z", + "updatedAt": "2021-05-09T21:34:38.700Z" + }, + { + "id": "fc577d14-78e8-404c-a17b-ab496e4041d8", + "workPeriodId": "fd6034b3-b6a0-4a3f-9a5d-fc077c08c680", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 29.32, + "customerRate": 29.89, + "days": 2, + "amount": 11.73, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:41.520Z", + "updatedAt": "2021-05-09T21:34:41.520Z" + } + ] }, { - "id": "333bcf25-25cc-4a4e-a6d7-2666a1326d68", - "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", - "userHandle": "zxx.lotus", - "projectId": 16870, - "startDate": "2021-01-17", - "endDate": "2021-01-23", + "id": "ac0ae325-8d77-4a73-bd85-5361165801cd", + "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "userHandle": "lakshmiaconnmgr", + "projectId": 111, + "startDate": "2021-06-06", + "endDate": "2021-06-12", + "daysWorked": 3, + "daysPaid": 3, + "paymentTotal": 8.25, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:15.951Z", + "updatedAt": "2021-05-09T21:46:12.086Z", + "payments": [ + { + "id": "00640d2d-8330-445a-b022-aa687033b2b3", + "workPeriodId": "ac0ae325-8d77-4a73-bd85-5361165801cd", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 13.74, + "customerRate": 14.57, + "days": 1, + "amount": 2.75, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:54.315Z", + "updatedAt": "2021-05-09T21:33:54.315Z" + }, + { + "id": "05d09e2b-02a0-4d33-b6db-0f69a98154c6", + "workPeriodId": "ac0ae325-8d77-4a73-bd85-5361165801cd", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 13.74, + "customerRate": 14.57, + "days": 2, + "amount": 5.5, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:53.131Z", + "updatedAt": "2021-05-09T21:33:53.131Z" + } + ] + }, + { + "id": "4ae26d58-1910-4ace-9f09-4c62c8df6031", + "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "userHandle": "lakshmiaconnmgr", + "projectId": 111, + "startDate": "2021-06-13", + "endDate": "2021-06-19", + "daysWorked": 3, + "daysPaid": 3, + "paymentTotal": 9.37, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:27:15.972Z", + "updatedAt": "2021-05-09T21:45:57.739Z", + "payments": [ + { + "id": "e623d9c7-caa9-4e58-83f1-44741b1169f8", + "workPeriodId": "4ae26d58-1910-4ace-9f09-4c62c8df6031", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 15.61, + "customerRate": 2.1, + "days": 3, + "amount": 9.37, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:19.371Z", + "updatedAt": "2021-05-09T21:32:19.371Z" + } + ] + }, + { + "id": "9d914249-82f2-422e-9ba6-c281da411266", + "resourceBookingId": "c4e86eae-ead3-49b7-90ab-92c3b4a3be0c", + "userHandle": "lakshmiaconnmgr", + "projectId": 111, + "startDate": "2021-05-16", + "endDate": "2021-05-22", "daysWorked": 5, - "memberRate": 162.3, - "customerRate": 132.28, - "paymentStatus": "pending", + "daysPaid": 5, + "paymentTotal": 113.23, + "paymentStatus": "completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:16.001Z", - "updatedAt": "2021-05-30T16:05:26.263Z", - "payments": [] + "createdAt": "2021-05-09T21:27:16.017Z", + "updatedAt": "2021-05-09T21:46:21.053Z", + "payments": [ + { + "id": "6036c7cc-b180-4f50-8163-5fd2541c66b5", + "workPeriodId": "9d914249-82f2-422e-9ba6-c281da411266", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 113.23, + "customerRate": 9.46, + "days": 5, + "amount": 113.23, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:40.984Z", + "updatedAt": "2021-05-09T21:33:40.984Z" + } + ] } ] }, { - "id": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", - "projectId": 17232, - "userId": "1ab93e53-71f6-4c50-ab48-9446229b6451", - "jobId": "ff76b81d-f49b-4019-b50e-c7932a818f19", + "id": "1ad758ab-c19f-4247-954a-4581420aba8a", + "projectId": 17363, + "userId": "dbf68f12-69a4-4592-a0ab-cf68d9ed7ae4", + "jobId": "fd48d96e-b0f2-43b7-8a48-f4fa194d6bc8", "status": "placed", - "startDate": "2021-01-01", - "endDate": "2021-02-01", - "memberRate": 18.4, - "customerRate": 84.88, + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 114.33, + "customerRate": 217.99, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-17T13:12:55.459Z", - "updatedAt": "2021-05-30T11:48:11.067Z", + "createdAt": "2021-05-27T11:25:35.292Z", + "updatedAt": "2021-05-30T11:49:08.019Z", "workPeriods": [ { - "id": "e84de57c-d131-4431-8e46-9452218d30e7", - "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", - "userHandle": "droopy74", - "projectId": 17232, - "startDate": "2021-01-03", - "endDate": "2021-01-09", - "daysWorked": 5, - "memberRate": 253.31, - "customerRate": 57.88, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:12.550Z", - "updatedAt": "2021-05-30T16:03:08.981Z", - "payments": [] - }, - { - "id": "97dabae3-74dd-45f6-ab83-53e4a828c4a6", - "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", - "userHandle": "droopy74", - "projectId": 17232, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 1, - "memberRate": 297.19, - "customerRate": 280.07, + "id": "8ff2339f-d90a-4ac2-9798-3158d0746d53", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:12.488Z", - "updatedAt": "2021-05-30T16:11:34.545Z", + "createdAt": "2021-05-30T11:49:08.875Z", + "updatedAt": "2021-05-30T16:12:09.701Z", "payments": [] }, { - "id": "75c345fa-9e34-46a9-8cf0-46245495110d", - "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", - "userHandle": "droopy74", - "projectId": 17232, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "c4c8588e-68a4-4b82-be91-a3d98661ffba", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, + "startDate": "2021-01-17", + "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 236.08, - "customerRate": 26.72, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:12.586Z", - "updatedAt": "2021-05-30T16:04:33.475Z", + "createdAt": "2021-05-30T11:49:08.879Z", + "updatedAt": "2021-05-30T16:03:35.487Z", "payments": [] }, { - "id": "20242864-9dd6-4376-85fb-1402297e4597", - "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", - "userHandle": "droopy74", - "projectId": 17232, - "startDate": "2021-01-17", - "endDate": "2021-01-23", + "id": "5ecaa40c-1fb3-4df7-9870-6fc3c2bc1bca", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 214.14, - "customerRate": 67.44, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:12.497Z", - "updatedAt": "2021-05-30T16:05:41.521Z", + "createdAt": "2021-05-30T11:49:08.877Z", + "updatedAt": "2021-05-30T16:04:53.463Z", "payments": [] }, { - "id": "c0c613e3-845a-45c0-85fe-41c063d9df3d", - "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", - "userHandle": "droopy74", - "projectId": 17232, + "id": "38afaa09-32da-4d81-b2f5-0c5e31af617f", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, "startDate": "2021-01-31", "endDate": "2021-02-06", - "daysWorked": 1, - "memberRate": 33.79, - "customerRate": 282.9, + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:12.493Z", - "updatedAt": "2021-05-30T16:11:34.546Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:08.867Z", + "updatedAt": "2021-05-30T16:05:21.960Z", "payments": [] }, { - "id": "b53b6f48-0a07-434e-a03d-f5d9ab772e60", - "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", - "userHandle": "droopy74", - "projectId": 17232, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": 4, - "memberRate": 58.11, - "customerRate": 256.06, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", + "id": "83cb4174-6ee3-4557-97c1-120c46054af6", + "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", + "userHandle": "vishalgoel", + "projectId": 17363, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:12.515Z", - "updatedAt": "2021-05-30T16:03:53.254Z", + "createdAt": "2021-05-30T11:49:08.882Z", + "updatedAt": "2021-05-30T16:04:22.560Z", "payments": [] } ] }, { - "id": "f667a667-6026-4d93-89bb-358aced982e5", - "projectId": 16870, - "userId": "2bba34d5-20e4-46d6-bfc1-05736b17afbb", - "jobId": "fed687e1-4257-48bb-806c-38712f9bf14f", + "id": "07f73049-e51a-4394-b61f-b75418afa908", + "projectId": 16739, + "userId": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "jobId": "fc5ba131-566f-46fe-8501-79c593241896", "status": "placed", - "startDate": "2021-01-03", - "endDate": "2021-02-03", - "memberRate": 61.4, + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 66, "customerRate": 114.05, - "rateType": "daily", + "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-04-29T09:28:04.405Z", - "updatedAt": "2021-05-30T11:48:13.500Z", + "createdAt": "2021-01-12T10:59:20.006Z", + "updatedAt": "2021-05-30T11:49:21.188Z", "workPeriods": [ { - "id": "1dd649b8-f536-4285-989d-56c45f1fca4d", - "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", - "userHandle": "GunaK-TopCoder", - "projectId": 16870, - "startDate": "2021-01-17", - "endDate": "2021-01-23", - "daysWorked": null, - "memberRate": 21.8, - "customerRate": 54.36, - "paymentStatus": "partially-completed", + "id": "7ff4804e-2a65-4f8b-af5b-24b58c066fd4", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:14.325Z", - "updatedAt": "2021-05-30T16:05:45.949Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:22.068Z", + "updatedAt": "2021-05-30T16:12:16.993Z", "payments": [] }, { - "id": "d44b8f1a-46b7-43a8-afd6-d2d13bd02fa5", - "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", - "userHandle": "GunaK-TopCoder", - "projectId": 16870, - "startDate": "2021-01-03", - "endDate": "2021-01-09", - "daysWorked": 5, - "memberRate": 298.88, - "customerRate": 84.09, + "id": "410e034d-ee48-4a18-aa65-679ef7efcb80", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:14.367Z", - "updatedAt": "2021-05-30T16:03:23.275Z", + "createdAt": "2021-05-30T11:49:21.961Z", + "updatedAt": "2021-05-30T16:05:16.466Z", "payments": [] }, { - "id": "7011b330-8509-4f60-a2db-c0f5c9b5837b", - "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", - "userHandle": "GunaK-TopCoder", - "projectId": 16870, + "id": "d99a524f-c8a4-4d46-a42c-dbcddd65b6db", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, "startDate": "2021-01-31", "endDate": "2021-02-06", - "daysWorked": 3, - "memberRate": 47.41, - "customerRate": 18.13, + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:14.320Z", - "updatedAt": "2021-05-30T16:11:35.512Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:22.017Z", + "updatedAt": "2021-05-30T16:03:19.706Z", "payments": [] }, { - "id": "8252cd13-4351-4a86-9315-521963f329f5", - "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", - "userHandle": "GunaK-TopCoder", - "projectId": 16870, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "5c4bb82b-e617-4c91-861f-5e0825d43c53", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 63.04, - "customerRate": 209, - "paymentStatus": "partially-completed", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:14.322Z", - "updatedAt": "2021-05-30T16:04:23.465Z", + "createdAt": "2021-05-30T11:49:21.977Z", + "updatedAt": "2021-05-30T16:04:55.294Z", "payments": [] }, { - "id": "259d8de1-1c56-49d0-936d-d5fcbdcc5a8a", - "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", - "userHandle": "GunaK-TopCoder", - "projectId": 16870, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": null, - "memberRate": 162.12, - "customerRate": 99.32, + "id": "da043aba-161e-4894-a3d5-d63678ac89b0", + "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", + "userHandle": "nkumartest", + "projectId": 16739, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:14.314Z", - "updatedAt": "2021-05-30T16:05:34.486Z", + "createdAt": "2021-05-30T11:49:21.974Z", + "updatedAt": "2021-05-30T16:03:18.831Z", "payments": [] } ] }, { - "id": "d84082de-9a09-4e9b-b5ab-4024f67687c5", - "projectId": 17103, - "userId": "fa5f4dc4-2992-4066-b4cc-16ceb5d1c1b7", - "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", - "status": "placed", - "startDate": "2021-01-11", - "endDate": "2021-02-11", - "memberRate": 287.14, - "customerRate": 258.37, - "rateType": "daily", + "id": "016312f6-72cf-486b-be8f-956ca4b2171e", + "projectId": 16706, + "userId": "4b00d029-c87b-47b2-bfe2-0ab80d8b5774", + "jobId": "fc2b006d-997b-49c3-a414-59ee54a48f9f", + "status": "sourcing", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 114.33, + "customerRate": 67.93, + "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-02T06:00:42.366Z", - "updatedAt": "2021-05-30T11:48:23.808Z", + "createdAt": "2021-01-26T08:27:38.010Z", + "updatedAt": "2021-05-30T11:40:20.936Z", "workPeriods": [ { - "id": "b60da405-cee8-41c9-919b-98d9acfd9f74", - "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", - "userHandle": "rtuthaya", - "projectId": 17103, + "id": "a2e2905a-efd7-45ba-a891-f0523b4b1351", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, "startDate": "2021-01-17", "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 271.31, - "customerRate": 195.92, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:24.607Z", - "updatedAt": "2021-05-30T16:03:52.385Z", + "createdAt": "2021-05-30T11:40:21.794Z", + "updatedAt": "2021-05-30T16:04:08.375Z", "payments": [] }, { - "id": "19110441-201e-449b-a350-661c50fb1387", - "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", - "userHandle": "rtuthaya", - "projectId": 17103, + "id": "6629d501-d17f-4261-a4d1-ed51d2a4b533", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, "startDate": "2021-01-24", "endDate": "2021-01-30", - "daysWorked": 4, - "memberRate": 30.12, - "customerRate": 84.76, - "paymentStatus": "cancelled", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:24.639Z", - "updatedAt": "2021-05-30T16:05:49.510Z", - "payments": [] - }, - { - "id": "1f12ada9-d0e6-43df-abe5-8a78850b20b4", - "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", - "userHandle": "rtuthaya", - "projectId": 17103, - "startDate": "2021-02-07", - "endDate": "2021-02-13", - "daysWorked": 4, - "memberRate": 191.92, - "customerRate": 6.17, - "paymentStatus": "cancelled", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:24.601Z", - "updatedAt": "2021-05-30T16:11:41.938Z", + "createdAt": "2021-05-30T11:40:21.784Z", + "updatedAt": "2021-05-30T16:04:46.695Z", "payments": [] }, { - "id": "1e94f892-ae25-4233-b9b0-81aa70c00b1e", - "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", - "userHandle": "rtuthaya", - "projectId": 17103, + "id": "42ca2389-e8a6-42c5-8a97-8d47531d2f23", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, "startDate": "2021-01-10", "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 109.93, - "customerRate": 271.9, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:24.604Z", - "updatedAt": "2021-05-30T16:05:45.069Z", + "createdAt": "2021-05-30T11:40:21.766Z", + "updatedAt": "2021-05-30T16:05:14.728Z", "payments": [] }, { - "id": "4d7f0350-b2f0-4f31-acc3-6555c3756fdd", - "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", - "userHandle": "rtuthaya", - "projectId": 17103, + "id": "b3101b68-cc83-4a5c-aa33-0c5220e4b78f", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, "startDate": "2021-01-31", "endDate": "2021-02-06", - "daysWorked": 5, - "memberRate": 70.84, - "customerRate": 207.33, - "paymentStatus": "partially-completed", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:24.637Z", - "updatedAt": "2021-05-30T16:05:08.370Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:40:21.780Z", + "updatedAt": "2021-05-30T16:12:19.637Z", "payments": [] - } - ] - }, - { - "id": "e673b52e-738d-47f9-bf37-68f6b5ed1926", - "projectId": 16870, - "userId": "60d3e956-820b-4d59-a30b-9309b838fac5", - "jobId": "fe539bef-9119-4a8c-b7b0-915e7e3a3ba3", - "status": "placed", - "startDate": "2021-01-01", - "endDate": "2021-02-01", - "memberRate": 60.63, - "customerRate": 132.43, - "rateType": "weekly", - "billingAccountId": 80000071, - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-04-30T08:09:51.618Z", - "updatedAt": "2021-05-30T11:48:20.311Z", - "workPeriods": [ + }, { - "id": "6ad492cf-4d90-4608-8dd5-13aaafad12e2", - "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", - "userHandle": "Hypernova", - "projectId": 16870, + "id": "149f2401-f2c2-4e3f-98fe-44148820cd5e", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, "startDate": "2020-12-27", "endDate": "2021-01-02", "daysWorked": 1, - "memberRate": 131.12, - "customerRate": 18.28, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:21.194Z", - "updatedAt": "2021-05-30T16:11:39.976Z", + "createdAt": "2021-05-30T11:40:21.757Z", + "updatedAt": "2021-05-30T16:12:19.636Z", "payments": [] }, { - "id": "61134c36-5e69-469c-bc50-75648f7949ca", - "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", - "userHandle": "Hypernova", - "projectId": 16870, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": 5, - "memberRate": 112.54, - "customerRate": 222.98, + "id": "e002f23e-8358-4f49-8770-af19aa23708e", + "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", + "userHandle": "nkumar2", + "projectId": 16706, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:21.187Z", - "updatedAt": "2021-05-30T16:04:51.659Z", + "createdAt": "2021-05-30T11:40:21.710Z", + "updatedAt": "2021-05-30T16:03:14.321Z", "payments": [] - }, + } + ] + }, + { + "id": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "projectId": 111, + "userId": "6d0509c7-5f12-4d84-9a19-8e80ef7ddd66", + "jobId": "a8adb1f8-a6ee-48b1-8661-33bd851b726e", + "status": "closed", + "startDate": "2021-03-27", + "endDate": "2021-04-27", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:26:16.403Z", + "updatedAt": "2021-05-09T21:26:16.403Z", + "workPeriods": [ { - "id": "f095424c-9a15-4f37-b8e4-1cd685f17451", - "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", - "userHandle": "Hypernova", - "projectId": 16870, - "startDate": "2021-01-31", - "endDate": "2021-02-06", - "daysWorked": 1, - "memberRate": 200.17, - "customerRate": 260.87, - "paymentStatus": "pending", + "id": "b0758857-0221-47a5-a444-e263e5d9e1cf", + "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "userHandle": "aaaa", + "projectId": 111, + "startDate": "2021-03-21", + "endDate": "2021-03-27", + "daysWorked": 5, + "daysPaid": 5, + "paymentTotal": 27.06, + "paymentStatus": "completed", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:21.198Z", - "updatedAt": "2021-05-30T16:11:39.977Z", - "payments": [] + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:17.187Z", + "updatedAt": "2021-05-09T21:46:59.354Z", + "payments": [ + { + "id": "9785ae89-05dc-4bcc-a030-52bd0e681d41", + "workPeriodId": "b0758857-0221-47a5-a444-e263e5d9e1cf", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 27.06, + "customerRate": 22.77, + "days": 5, + "amount": 27.06, + "status": "cancelled", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:01.410Z", + "updatedAt": "2021-05-09T21:34:01.410Z" + }, + { + "id": "c8be508d-2eb5-4712-8bd7-1b28e870abc2", + "workPeriodId": "b0758857-0221-47a5-a444-e263e5d9e1cf", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 27.06, + "customerRate": 22.77, + "days": 5, + "amount": 27.06, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:34:02.733Z", + "updatedAt": "2021-05-09T21:34:02.733Z" + } + ] }, { - "id": "f79b0a83-5f72-4e9c-bffe-4ebf694db7f4", - "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", - "userHandle": "Hypernova", - "projectId": 16870, - "startDate": "2021-01-17", - "endDate": "2021-01-23", + "id": "176db0d0-474f-4590-831a-547d596c01b4", + "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "userHandle": "aaaa", + "projectId": 111, + "startDate": "2021-03-28", + "endDate": "2021-04-03", "daysWorked": 5, - "memberRate": 27.09, - "customerRate": 40.76, - "paymentStatus": "pending", + "daysPaid": 5, + "paymentTotal": 6.11, + "paymentStatus": "completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:21.107Z", - "updatedAt": "2021-05-30T16:02:50.987Z", - "payments": [] + "createdAt": "2021-05-09T21:26:17.168Z", + "updatedAt": "2021-05-09T21:47:05.373Z", + "payments": [ + { + "id": "c3b71f96-7680-459b-82b2-f6eb3c3f6c8f", + "workPeriodId": "176db0d0-474f-4590-831a-547d596c01b4", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 6.11, + "customerRate": 14.63, + "days": 1, + "amount": 1.22, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:18.342Z", + "updatedAt": "2021-05-09T21:31:18.342Z" + }, + { + "id": "3ed31706-0e99-4084-81f4-b126a1a68db6", + "workPeriodId": "176db0d0-474f-4590-831a-547d596c01b4", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 6.11, + "customerRate": 14.63, + "days": 4, + "amount": 4.89, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:17.193Z", + "updatedAt": "2021-05-09T21:31:17.193Z" + } + ] }, { - "id": "c75ee2eb-a7c0-4eb8-83f9-860ce22d1b03", - "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", - "userHandle": "Hypernova", - "projectId": 16870, - "startDate": "2021-01-03", - "endDate": "2021-01-09", - "daysWorked": 5, - "memberRate": 278.4, - "customerRate": 270.68, + "id": "8c9db4fd-78ad-4e59-acba-462487b74c3a", + "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "userHandle": "aaaa", + "projectId": 111, + "startDate": "2021-04-18", + "endDate": "2021-04-24", + "daysWorked": 2, + "daysPaid": 2, + "paymentTotal": 10.98, "paymentStatus": "completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:21.104Z", - "updatedAt": "2021-05-30T16:03:34.616Z", - "payments": [] + "createdAt": "2021-05-09T21:26:17.200Z", + "updatedAt": "2021-05-09T21:47:25.687Z", + "payments": [ + { + "id": "f65930b7-d61d-4923-bdab-54848661f151", + "workPeriodId": "8c9db4fd-78ad-4e59-acba-462487b74c3a", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 27.46, + "customerRate": 25.57, + "days": 1, + "amount": 5.49, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:06.108Z", + "updatedAt": "2021-05-09T21:33:06.108Z" + }, + { + "id": "8eb8fc37-5ab0-4480-8806-3d3c57ab38e1", + "workPeriodId": "8c9db4fd-78ad-4e59-acba-462487b74c3a", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 27.46, + "customerRate": 25.57, + "days": 1, + "amount": 5.49, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:07.419Z", + "updatedAt": "2021-05-09T21:33:07.419Z" + } + ] }, { - "id": "b0aad7c5-bafb-4cae-90ca-832334505e9b", - "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", - "userHandle": "Hypernova", - "projectId": 16870, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "9b455e21-e186-4622-923a-f115d23549d1", + "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "userHandle": "aaaa", + "projectId": 111, + "startDate": "2021-04-25", + "endDate": "2021-05-01", + "daysWorked": 2, + "daysPaid": 2, + "paymentTotal": 9.13, + "paymentStatus": "completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:17.161Z", + "updatedAt": "2021-05-09T21:47:19.034Z", + "payments": [ + { + "id": "10584b23-5ab2-44e2-a927-e020c08e4f84", + "workPeriodId": "9b455e21-e186-4622-923a-f115d23549d1", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 22.82, + "customerRate": 10.1, + "days": 2, + "amount": 9.13, + "status": "cancelled", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:29.348Z", + "updatedAt": "2021-05-09T21:33:29.348Z" + }, + { + "id": "6d59a499-44e3-41e2-8368-5baee86dd8ab", + "workPeriodId": "9b455e21-e186-4622-923a-f115d23549d1", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 22.82, + "customerRate": 10.1, + "days": 2, + "amount": 9.13, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:33:28.108Z", + "updatedAt": "2021-05-09T21:33:28.108Z" + } + ] + }, + { + "id": "5a174833-cb08-49f5-b077-cffb8e60ca01", + "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "userHandle": "aaaa", + "projectId": 111, + "startDate": "2021-04-04", + "endDate": "2021-04-10", "daysWorked": 5, - "memberRate": 155.75, - "customerRate": 13.09, - "paymentStatus": "pending", + "daysPaid": 3, + "paymentTotal": 2.95, + "paymentStatus": "partially-completed", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:21.192Z", - "updatedAt": "2021-05-30T16:03:58.410Z", - "payments": [] + "createdAt": "2021-05-09T21:26:17.165Z", + "updatedAt": "2021-05-09T21:47:12.015Z", + "payments": [ + { + "id": "663f11df-7832-431a-a46e-ad8c890ae52b", + "workPeriodId": "5a174833-cb08-49f5-b077-cffb8e60ca01", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 4.91, + "customerRate": 24.11, + "days": 5, + "amount": 4.91, + "status": "cancelled", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:32.606Z", + "updatedAt": "2021-05-09T21:32:32.606Z" + }, + { + "id": "69445cbf-6d94-49a5-b2aa-65459ec78594", + "workPeriodId": "5a174833-cb08-49f5-b077-cffb8e60ca01", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 4.91, + "customerRate": 24.11, + "days": 3, + "amount": 2.95, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:32:33.664Z", + "updatedAt": "2021-05-09T21:32:33.664Z" + } + ] + }, + { + "id": "18881107-cc17-4087-9b2b-a74f04187f73", + "resourceBookingId": "d5de4575-c977-4f4b-bd5b-bb82360dd365", + "userHandle": "aaaa", + "projectId": 111, + "startDate": "2021-04-11", + "endDate": "2021-04-17", + "daysWorked": 2, + "daysPaid": 1, + "paymentTotal": 4.56, + "paymentStatus": "partially-completed", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-09T21:26:17.202Z", + "updatedAt": "2021-05-09T21:47:30.586Z", + "payments": [ + { + "id": "dd8f5c08-d6a1-4fd2-b6bd-85fb6425d13d", + "workPeriodId": "18881107-cc17-4087-9b2b-a74f04187f73", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 22.82, + "customerRate": 2.32, + "days": 1, + "amount": 4.56, + "status": "completed", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:25.690Z", + "updatedAt": "2021-05-09T21:31:25.690Z" + }, + { + "id": "c456755e-0432-4656-848b-64f9c5dc8f25", + "workPeriodId": "18881107-cc17-4087-9b2b-a74f04187f73", + "challengeId": "00000000-0000-0000-0000-000000000000", + "memberRate": 22.82, + "customerRate": 2.32, + "days": 2, + "amount": 9.13, + "status": "cancelled", + "statusDetails": null, + "billingAccountId": 80000071, + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "updatedBy": null, + "createdAt": "2021-05-09T21:31:27.102Z", + "updatedAt": "2021-05-09T21:31:27.102Z" + } + ] } ] }, { - "id": "016312f6-72cf-486b-be8f-956ca4b2171e", - "projectId": 16706, - "userId": "4b00d029-c87b-47b2-bfe2-0ab80d8b5774", - "jobId": "fc2b006d-997b-49c3-a414-59ee54a48f9f", + "id": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "projectId": 16718, + "userId": "085fc95d-0336-4572-a641-6c8334e7f0c9", + "jobId": "fb2f5f9b-5874-4dcd-af94-727fc0409760", "status": "sourcing", "startDate": "2021-01-01", "endDate": "2021-02-01", "memberRate": 114.33, - "customerRate": 67.93, + "customerRate": 100.25, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-26T08:27:38.010Z", - "updatedAt": "2021-05-30T11:40:20.936Z", + "createdAt": "2021-01-26T08:27:33.886Z", + "updatedAt": "2021-05-30T11:49:22.899Z", "workPeriods": [ { - "id": "6629d501-d17f-4261-a4d1-ed51d2a4b533", - "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", - "userHandle": "nkumar2", - "projectId": 16706, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "f8d56b84-b374-4975-81f8-7fab96463243", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2021-01-03", + "endDate": "2021-01-09", "daysWorked": 5, - "memberRate": 181.55, - "customerRate": 22.69, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:23.666Z", + "updatedAt": "2021-05-30T16:02:49.121Z", + "payments": [] + }, + { + "id": "ddcfc959-d749-45dc-9e9f-f18a893f9e1a", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:40:21.784Z", - "updatedAt": "2021-05-30T16:04:46.695Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:23.644Z", + "updatedAt": "2021-05-30T16:12:17.859Z", "payments": [] }, { - "id": "42ca2389-e8a6-42c5-8a97-8d47531d2f23", - "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", - "userHandle": "nkumar2", - "projectId": 16706, - "startDate": "2021-01-10", - "endDate": "2021-01-16", + "id": "a322ee7e-7f23-4f9f-b2d8-286b574efd7f", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2021-01-24", + "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 41.26, - "customerRate": 286.6, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:40:21.766Z", - "updatedAt": "2021-05-30T16:05:14.728Z", + "createdAt": "2021-05-30T11:49:23.681Z", + "updatedAt": "2021-05-30T16:04:07.452Z", "payments": [] }, { - "id": "a2e2905a-efd7-45ba-a891-f0523b4b1351", - "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", - "userHandle": "nkumar2", - "projectId": 16706, + "id": "77f9b42c-6e67-4363-8b43-aa0b70a904e1", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, "startDate": "2021-01-17", "endDate": "2021-01-23", - "daysWorked": null, - "memberRate": 114.59, - "customerRate": 180.76, + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:40:21.794Z", - "updatedAt": "2021-05-30T16:04:08.375Z", + "createdAt": "2021-05-30T11:49:23.734Z", + "updatedAt": "2021-05-30T16:04:29.823Z", "payments": [] }, { - "id": "b3101b68-cc83-4a5c-aa33-0c5220e4b78f", - "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", - "userHandle": "nkumar2", - "projectId": 16706, + "id": "20fc029b-108a-4f12-aec9-ba36619d4ce7", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, "startDate": "2021-01-31", "endDate": "2021-02-06", "daysWorked": 1, - "memberRate": 280.73, - "customerRate": 7.02, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:40:21.780Z", - "updatedAt": "2021-05-30T16:12:19.637Z", - "payments": [] - }, - { - "id": "149f2401-f2c2-4e3f-98fe-44148820cd5e", - "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", - "userHandle": "nkumar2", - "projectId": 16706, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 1, - "memberRate": 3.94, - "customerRate": 88.91, - "paymentStatus": "partially-completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:40:21.757Z", - "updatedAt": "2021-05-30T16:12:19.636Z", + "createdAt": "2021-05-30T11:49:23.730Z", + "updatedAt": "2021-05-30T16:12:17.860Z", "payments": [] }, { - "id": "e002f23e-8358-4f49-8770-af19aa23708e", - "resourceBookingId": "016312f6-72cf-486b-be8f-956ca4b2171e", - "userHandle": "nkumar2", - "projectId": 16706, - "startDate": "2021-01-03", - "endDate": "2021-01-09", + "id": "662f11e5-c02e-460d-989e-1396ff4f00a6", + "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", + "userHandle": "george0095", + "projectId": 16718, + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 4, - "memberRate": 108.35, - "customerRate": 189.61, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:40:21.710Z", - "updatedAt": "2021-05-30T16:03:14.321Z", + "createdAt": "2021-05-30T11:49:23.678Z", + "updatedAt": "2021-05-30T16:04:45.827Z", "payments": [] } ] }, { - "id": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", - "projectId": 16899, - "userId": "5bc40e16-4fdb-40f1-93fe-de465789e1b2", - "jobId": "fe270791-bc24-4f6a-8c1b-b897f5d97d2f", + "id": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "projectId": 16870, + "userId": "b851684b-1071-47c3-8719-bdae96aa0e6d", + "jobId": "fed687e1-4257-48bb-806c-38712f9bf14f", "status": "placed", "startDate": "2021-01-11", "endDate": "2021-02-11", - "memberRate": 271.93, - "customerRate": 102.37, + "memberRate": 115.29, + "customerRate": 278.45, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-01T09:45:15.939Z", - "updatedAt": "2021-05-30T11:48:39.054Z", + "createdAt": "2021-04-29T09:50:13.905Z", + "updatedAt": "2021-05-30T11:48:15.196Z", "workPeriods": [ { - "id": "eeea5cf0-513c-4d1a-9318-a376aa86c28f", - "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", - "userHandle": "ramag", - "projectId": 16899, - "startDate": "2021-01-17", - "endDate": "2021-01-23", + "id": "e95f0541-bc37-4c1e-a619-029753c1a69a", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 298.22, - "customerRate": 277.1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:39.879Z", - "updatedAt": "2021-05-30T16:03:01.733Z", + "createdAt": "2021-05-30T11:48:16.045Z", + "updatedAt": "2021-05-30T16:03:08.035Z", "payments": [] }, { - "id": "ab2c5ad4-165f-48d2-bf1c-005b56e049ce", - "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", - "userHandle": "ramag", - "projectId": 16899, + "id": "cb6114ca-1624-4239-8243-aee05f8b5fe5", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:16.042Z", + "updatedAt": "2021-05-30T16:03:29.374Z", + "payments": [] + }, + { + "id": "f382c2c8-fe32-42f3-b960-29c345ab0264", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, "startDate": "2021-02-07", "endDate": "2021-02-13", "daysWorked": 4, - "memberRate": 174.63, - "customerRate": 103.63, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:39.824Z", - "updatedAt": "2021-05-30T16:11:51.073Z", + "createdAt": "2021-05-30T11:48:16.004Z", + "updatedAt": "2021-05-30T16:11:36.370Z", "payments": [] }, { - "id": "aed553d8-4eb4-45a8-86f0-3c21f81c7570", - "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", - "userHandle": "ramag", - "projectId": 16899, + "id": "9215b576-8f67-4511-88f9-76000d6c8326", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, "startDate": "2021-01-24", "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 19.08, - "customerRate": 101.27, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:39.870Z", - "updatedAt": "2021-05-30T16:04:00.297Z", + "createdAt": "2021-05-30T11:48:16.008Z", + "updatedAt": "2021-05-30T16:04:19.062Z", "payments": [] }, { - "id": "4c9a7d50-8014-4ff2-9867-98dc66e466ac", - "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", - "userHandle": "ramag", - "projectId": 16899, - "startDate": "2021-01-10", - "endDate": "2021-01-16", + "id": "333bcf25-25cc-4a4e-a6d7-2666a1326d68", + "resourceBookingId": "f40f43a8-b7b0-4181-967c-26e4e070f95e", + "userHandle": "zxx.lotus", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 209.34, - "customerRate": 97.01, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:39.838Z", - "updatedAt": "2021-05-30T16:05:09.308Z", - "payments": [] - }, - { - "id": "d5c4716f-e26a-4d3f-a57d-2410d7537ecd", - "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", - "userHandle": "ramag", - "projectId": 16899, - "startDate": "2021-01-31", - "endDate": "2021-02-06", - "daysWorked": null, - "memberRate": 30.95, - "customerRate": 156.32, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:39.820Z", - "updatedAt": "2021-05-30T16:03:21.454Z", + "createdAt": "2021-05-30T11:48:16.001Z", + "updatedAt": "2021-05-30T16:05:26.263Z", "payments": [] } ] }, { - "id": "48bd8a8b-40fb-459a-b5db-f22de90c2799", - "projectId": 16870, - "userId": "2bba34d5-20e4-46d6-bfc1-05736b17afbb", - "jobId": "fe539bef-9119-4a8c-b7b0-915e7e3a3ba3", - "status": "closed", - "startDate": "2021-01-02", - "endDate": "2021-02-02", - "memberRate": 18.4, - "customerRate": 30.14, + "id": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "projectId": 16762, + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "jobId": "fe481d1c-cf87-49c1-9370-695f9f754041", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 61.33, + "customerRate": 84.88, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-01T03:43:44.374Z", - "updatedAt": "2021-05-30T11:49:00.956Z", + "createdAt": "2021-01-12T10:53:32.373Z", + "updatedAt": "2021-05-30T11:48:49.532Z", "workPeriods": [ { - "id": "b9768ae4-ae40-4bb9-8bc2-970c9f36f0bf", - "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", - "userHandle": "GunaK-TopCoder", - "projectId": 16870, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 0, - "memberRate": 213.59, - "customerRate": 286.15, + "id": "3ad03850-ebe9-4227-8b30-1303b20bbd31", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:01.820Z", - "updatedAt": "2021-05-30T16:12:04.121Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:50.292Z", + "updatedAt": "2021-05-30T16:05:19.240Z", "payments": [] }, { - "id": "77ac0f07-d4ae-463e-a9db-437623a29958", - "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", - "userHandle": "GunaK-TopCoder", - "projectId": 16870, - "startDate": "2021-01-03", - "endDate": "2021-01-09", + "id": "2ea4bffd-2519-422f-8baa-a0f74b3b398b", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 191.95, - "customerRate": 172.18, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:01.793Z", - "updatedAt": "2021-05-30T16:04:31.663Z", + "createdAt": "2021-05-30T11:48:50.340Z", + "updatedAt": "2021-05-30T16:05:30.731Z", "payments": [] }, { - "id": "1f5d3abe-fe2b-4961-831c-b7bbdf76da82", - "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", - "userHandle": "GunaK-TopCoder", - "projectId": 16870, - "startDate": "2021-01-17", - "endDate": "2021-01-23", - "daysWorked": 5, - "memberRate": 54.98, - "customerRate": 237.88, + "id": "e1099a6a-7c6b-465d-bb1b-517c3fbd06f1", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:01.732Z", - "updatedAt": "2021-05-30T16:05:43.321Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:50.297Z", + "updatedAt": "2021-05-30T16:11:57.610Z", "payments": [] }, { - "id": "1c526f5f-d9e0-4e16-8421-cee4e8154a3c", - "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", - "userHandle": "GunaK-TopCoder", - "projectId": 16870, + "id": "a154b1fb-06d3-4cfd-97d3-0a810a1c4317", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, "startDate": "2021-01-24", "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 36.75, - "customerRate": 21.33, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:01.802Z", - "updatedAt": "2021-05-30T16:05:48.592Z", + "createdAt": "2021-05-30T11:48:50.279Z", + "updatedAt": "2021-05-30T16:04:09.261Z", "payments": [] }, { - "id": "b7bce7db-65dc-4447-8865-f6e8a84a867e", - "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", - "userHandle": "GunaK-TopCoder", - "projectId": 16870, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": 5, - "memberRate": 209.34, - "customerRate": 251.36, + "id": "a35207bd-ac1d-4539-b7bb-7a923c8a6f7f", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:01.698Z", - "updatedAt": "2021-05-30T16:03:48.778Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:50.330Z", + "updatedAt": "2021-05-30T16:11:57.609Z", "payments": [] }, { - "id": "a475c2fa-c5ce-4a30-bdd8-d4fad1e6308c", - "resourceBookingId": "48bd8a8b-40fb-459a-b5db-f22de90c2799", - "userHandle": "GunaK-TopCoder", - "projectId": 16870, - "startDate": "2021-01-31", - "endDate": "2021-02-06", - "daysWorked": 2, - "memberRate": 25.99, - "customerRate": 155.48, + "id": "b34361ca-eb0e-47f7-86d9-3bccbb6839d5", + "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", + "userHandle": "pshah_manager", + "projectId": 16762, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:01.795Z", - "updatedAt": "2021-05-30T16:12:04.120Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:50.288Z", + "updatedAt": "2021-05-30T16:03:55.011Z", "payments": [] } ] }, { - "id": "72829b1f-9183-4660-815f-d3e80d38a5a9", - "projectId": 17290, - "userId": "0eaf032f-f376-47cc-b7aa-668685efac90", - "jobId": "fe600350-0a6d-4dac-922f-a6a7d285daa1", + "id": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "projectId": 17324, + "userId": "4709473d-f060-4102-87f8-4d51ff0b34c1", + "jobId": "fefd2618-9b66-4431-9874-1d02d7a37d90", "status": "placed", - "startDate": "2021-01-01", - "endDate": "2021-02-01", - "memberRate": 106.51, - "customerRate": 111.21, + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 271.93, + "customerRate": 188.33, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-18T08:18:13.123Z", - "updatedAt": "2021-05-30T11:48:52.915Z", + "createdAt": "2021-05-20T06:52:40.679Z", + "updatedAt": "2021-05-30T11:49:15.876Z", "workPeriods": [ { - "id": "e6b70714-8bd6-47ce-9e58-f04d3c25ee28", - "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", - "userHandle": "gliu", - "projectId": 17290, - "startDate": "2021-01-24", - "endDate": "2021-01-30", - "daysWorked": 5, - "memberRate": 147.9, - "customerRate": 132.28, + "id": "f28bd617-dce3-47c0-a9ab-6b2ff321d206", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:53.719Z", - "updatedAt": "2021-05-30T16:03:09.841Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:16.700Z", + "updatedAt": "2021-05-30T16:12:14.202Z", "payments": [] }, { - "id": "b6c1f079-e5cb-46a0-a6bf-5988ec013c4c", - "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", - "userHandle": "gliu", - "projectId": 17290, - "startDate": "2021-01-03", - "endDate": "2021-01-09", + "id": "c447a850-2549-4c6a-ad3e-47cb6b26ac0b", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, + "startDate": "2021-01-24", + "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 22.66, - "customerRate": 265.36, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:53.776Z", - "updatedAt": "2021-05-30T16:03:49.745Z", + "createdAt": "2021-05-30T11:49:16.706Z", + "updatedAt": "2021-05-30T16:03:37.121Z", "payments": [] }, { - "id": "064d2511-9af6-4d6a-be4f-79eebacc6345", - "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", - "userHandle": "gliu", - "projectId": 17290, + "id": "66732a8f-7bab-4e46-8eda-c58f28344114", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, "startDate": "2021-01-31", "endDate": "2021-02-06", - "daysWorked": 1, - "memberRate": 66.85, - "customerRate": 16.02, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:53.778Z", - "updatedAt": "2021-05-30T16:11:59.550Z", - "payments": [] - }, - { - "id": "c8c69543-1598-43e4-9ef6-8a569ebdf831", - "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", - "userHandle": "gliu", - "projectId": 17290, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 1, - "memberRate": 252.02, - "customerRate": 271.75, + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:53.716Z", - "updatedAt": "2021-05-30T16:11:59.551Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:16.759Z", + "updatedAt": "2021-05-30T16:04:44.983Z", "payments": [] }, { - "id": "0d237fa9-3fe9-48dc-82b8-7027edddc5a1", - "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", - "userHandle": "gliu", - "projectId": 17290, + "id": "444fbe9a-616e-443a-a1b5-aadfe7c617ff", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, "startDate": "2021-01-10", "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 33.79, - "customerRate": 155.11, - "paymentStatus": "partially-completed", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:53.731Z", - "updatedAt": "2021-05-30T16:05:57.480Z", + "createdAt": "2021-05-30T11:49:16.687Z", + "updatedAt": "2021-05-30T16:05:13.852Z", "payments": [] }, { - "id": "7bc96a71-deda-4cde-b8b0-f809cea1398a", - "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", - "userHandle": "gliu", - "projectId": 17290, + "id": "50865971-2fd2-4576-a799-8ab438e9dd75", + "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", + "userHandle": "TCConnCopilot", + "projectId": 17324, "startDate": "2021-01-17", "endDate": "2021-01-23", - "daysWorked": 5, - "memberRate": 140.77, - "customerRate": 120.77, + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:53.710Z", - "updatedAt": "2021-05-30T16:04:26.082Z", + "createdAt": "2021-05-30T11:49:16.701Z", + "updatedAt": "2021-05-30T16:05:05.541Z", "payments": [] } ] }, { - "id": "c666e835-4145-406e-b6bb-8b0f98ed8f68", - "projectId": 17290, - "userId": "1f6ca39c-0620-4de0-9bb2-d64d4ce26b42", - "jobId": "fe600350-0a6d-4dac-922f-a6a7d285daa1", - "status": "placed", - "startDate": "2021-01-11", - "endDate": "2021-02-11", - "memberRate": 4.07, - "customerRate": 296.66, + "id": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "projectId": 17103, + "userId": "9e4b1242-9b14-4159-bd0b-de7fa1803ca9", + "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "status": "cancelled", + "startDate": "2021-01-21", + "endDate": "2021-02-21", + "memberRate": 61.33, + "customerRate": 196.21, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-18T07:05:53.664Z", - "updatedAt": "2021-05-30T11:48:33.177Z", + "createdAt": "2021-05-01T10:25:50.725Z", + "updatedAt": "2021-05-30T11:49:19.410Z", "workPeriods": [ { - "id": "e29735b3-cf81-4877-8fd5-6d346a1824f0", - "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", - "userHandle": "suacoustic", - "projectId": 17290, - "startDate": "2021-01-17", - "endDate": "2021-01-23", - "daysWorked": 5, - "memberRate": 8.1, - "customerRate": 273.89, - "paymentStatus": "completed", + "id": "f04d5bb8-abba-4447-92f0-005e823238f8", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-02-21", + "endDate": "2021-02-27", + "daysWorked": 0, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "noDays", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:33.942Z", - "updatedAt": "2021-05-30T16:03:12.511Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:20.193Z", + "updatedAt": "2021-05-30T16:12:15.996Z", "payments": [] }, { - "id": "df0b3604-7ee9-4862-a8d1-abe8a2142f77", - "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", - "userHandle": "suacoustic", - "projectId": 17290, - "startDate": "2021-01-31", - "endDate": "2021-02-06", + "id": "fe874682-2ba6-4f42-929b-efc9e05adafd", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-02-14", + "endDate": "2021-02-20", "daysWorked": 5, - "memberRate": 154, - "customerRate": 244.09, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:33.954Z", - "updatedAt": "2021-05-30T16:03:15.193Z", + "createdAt": "2021-05-30T11:49:20.197Z", + "updatedAt": "2021-05-30T15:58:40.816Z", "payments": [] }, { - "id": "9a480e44-2026-4327-a220-715ace30743e", - "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", - "userHandle": "suacoustic", - "projectId": 17290, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": 5, - "memberRate": 248.36, - "customerRate": 257.04, + "id": "217f124d-37db-49a8-9cac-187c5c8b2905", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:33.952Z", - "updatedAt": "2021-05-30T16:04:12.811Z", + "createdAt": "2021-05-30T11:49:20.202Z", + "updatedAt": "2021-05-30T16:05:38.038Z", "payments": [] }, { - "id": "b6147962-6666-4534-8b73-0c7f9a7052e8", - "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", - "userHandle": "suacoustic", - "projectId": 17290, - "startDate": "2021-02-07", - "endDate": "2021-02-13", - "daysWorked": 4, - "memberRate": 126.28, - "customerRate": 4.22, + "id": "b826c9b8-12f5-4567-bd62-df524bb690a2", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:33.959Z", - "updatedAt": "2021-05-30T16:11:47.528Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:20.194Z", + "updatedAt": "2021-05-30T16:03:47.803Z", "payments": [] }, { - "id": "47ac2474-d9e9-416d-afa8-fea8fb4f2a6c", - "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", - "userHandle": "suacoustic", - "projectId": 17290, + "id": "3e6436c6-f6d4-4b33-8027-1269b167554f", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, "startDate": "2021-01-24", "endDate": "2021-01-30", - "daysWorked": 4, - "memberRate": 28.27, - "customerRate": 256.06, + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:33.940Z", - "updatedAt": "2021-05-30T16:05:12.030Z", + "createdAt": "2021-05-30T11:49:20.200Z", + "updatedAt": "2021-05-30T16:05:18.330Z", + "payments": [] + }, + { + "id": "56f49073-e651-479c-967c-8ba58e36b8e6", + "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", + "userHandle": "ApolloZhang", + "projectId": 17103, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 2, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:20.236Z", + "updatedAt": "2021-05-30T16:12:15.994Z", "payments": [] } ] }, { - "id": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", - "projectId": 16781, - "userId": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "jobId": "ff3feeae-d4f7-457c-bff7-215be5efe2b8", + "id": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "projectId": 17232, + "userId": "1ab93e53-71f6-4c50-ab48-9446229b6451", + "jobId": "ff76b81d-f49b-4019-b50e-c7932a818f19", "status": "placed", "startDate": "2021-01-01", "endDate": "2021-02-01", - "memberRate": 54.02, - "customerRate": 217.99, + "memberRate": 18.4, + "customerRate": 84.88, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-12T15:02:15.210Z", - "updatedAt": "2021-05-30T11:48:59.149Z", + "createdAt": "2021-05-17T13:12:55.459Z", + "updatedAt": "2021-05-30T11:48:11.067Z", "workPeriods": [ { - "id": "d062e2fe-2446-4fb5-b0b3-0577cd57fd7c", - "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", - "userHandle": "nkumartest", - "projectId": 16781, - "startDate": "2021-01-17", - "endDate": "2021-01-23", + "id": "e84de57c-d131-4431-8e46-9452218d30e7", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-03", + "endDate": "2021-01-09", "daysWorked": 5, - "memberRate": 122.15, - "customerRate": 115.26, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:59.909Z", - "updatedAt": "2021-05-30T16:03:24.198Z", + "createdAt": "2021-05-30T11:48:12.550Z", + "updatedAt": "2021-05-30T16:03:08.981Z", "payments": [] }, { - "id": "bdd60068-9e8d-4c43-8440-48a066ba4396", - "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", - "userHandle": "nkumartest", - "projectId": 16781, - "startDate": "2021-01-31", - "endDate": "2021-02-06", + "id": "97dabae3-74dd-45f6-ab83-53e4a828c4a6", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2020-12-27", + "endDate": "2021-01-02", "daysWorked": 1, - "memberRate": 15.41, - "customerRate": 55.77, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:59.921Z", - "updatedAt": "2021-05-30T16:12:03.178Z", + "createdAt": "2021-05-30T11:48:12.488Z", + "updatedAt": "2021-05-30T16:11:34.545Z", "payments": [] }, { - "id": "a0eec246-3b4d-41b6-9d54-b892513bd727", - "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", - "userHandle": "nkumartest", - "projectId": 16781, - "startDate": "2021-01-10", - "endDate": "2021-01-16", + "id": "75c345fa-9e34-46a9-8cf0-46245495110d", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-24", + "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 231.38, - "customerRate": 270.68, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:00.007Z", - "updatedAt": "2021-05-30T16:04:10.173Z", + "createdAt": "2021-05-30T11:48:12.586Z", + "updatedAt": "2021-05-30T16:04:33.475Z", "payments": [] }, { - "id": "752f3cf3-a9c2-487c-93ae-22d21af10403", - "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", - "userHandle": "nkumartest", - "projectId": 16781, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "20242864-9dd6-4376-85fb-1402297e4597", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-17", + "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 66.85, - "customerRate": 42.45, - "paymentStatus": "completed", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:00.041Z", - "updatedAt": "2021-05-30T16:04:34.403Z", + "createdAt": "2021-05-30T11:48:12.497Z", + "updatedAt": "2021-05-30T16:05:41.521Z", "payments": [] }, { - "id": "5be5a577-32cd-4557-8d11-bb5723dd7be2", - "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", - "userHandle": "nkumartest", - "projectId": 16781, - "startDate": "2021-01-03", - "endDate": "2021-01-09", - "daysWorked": 5, - "memberRate": 158.5, - "customerRate": 299.34, + "id": "c0c613e3-845a-45c0-85fe-41c063d9df3d", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:59.998Z", - "updatedAt": "2021-05-30T16:04:57.065Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:12.493Z", + "updatedAt": "2021-05-30T16:11:34.546Z", "payments": [] }, { - "id": "e9ff7b7f-05ec-45f9-a09e-b2c24017b59b", - "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", - "userHandle": "nkumartest", - "projectId": 16781, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 1, - "memberRate": 34.56, - "customerRate": 175.32, + "id": "b53b6f48-0a07-434e-a03d-f5d9ab772e60", + "resourceBookingId": "f88e8c6b-565a-41ca-a8b4-72351fc140fe", + "userHandle": "droopy74", + "projectId": 17232, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:59.974Z", - "updatedAt": "2021-05-30T16:12:03.180Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:12.515Z", + "updatedAt": "2021-05-30T16:03:53.254Z", "payments": [] } ] }, { - "id": "a74df62a-dba0-4214-8f8e-5e071f359afe", - "projectId": 17363, - "userId": "c40cdb0a-4fac-4ca1-8052-d92001858887", - "jobId": "fd48d96e-b0f2-43b7-8a48-f4fa194d6bc8", + "id": "f667a667-6026-4d93-89bb-358aced982e5", + "projectId": 16870, + "userId": "2bba34d5-20e4-46d6-bfc1-05736b17afbb", + "jobId": "fed687e1-4257-48bb-806c-38712f9bf14f", "status": "placed", - "startDate": "2021-01-01", - "endDate": "2021-02-01", - "memberRate": 235.26, - "customerRate": 84.88, - "rateType": "hourly", + "startDate": "2021-01-03", + "endDate": "2021-02-03", + "memberRate": 61.4, + "customerRate": 114.05, + "rateType": "daily", "billingAccountId": 80000071, - "createdBy": "71c5e6a8-51d9-4fb5-91ce-d974642531af", + "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-28T04:41:39.728Z", - "updatedAt": "2021-05-30T11:48:34.793Z", + "createdAt": "2021-04-29T09:28:04.405Z", + "updatedAt": "2021-05-30T11:48:13.500Z", "workPeriods": [ { - "id": "94a897e9-6291-4206-a0b1-74c35ff06a6e", - "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", - "userHandle": "ApolloChang", - "projectId": 17363, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": null, - "memberRate": 214.34, - "customerRate": 16.02, + "id": "d44b8f1a-46b7-43a8-afd6-d2d13bd02fa5", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:35.596Z", - "updatedAt": "2021-05-30T16:04:17.285Z", + "createdAt": "2021-05-30T11:48:14.367Z", + "updatedAt": "2021-05-30T16:03:23.275Z", "payments": [] }, { - "id": "2570737c-02b4-4b90-b692-45dcc5774215", - "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", - "userHandle": "ApolloChang", - "projectId": 17363, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 1, - "memberRate": 34.23, - "customerRate": 257.04, + "id": "7011b330-8509-4f60-a2db-c0f5c9b5837b", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 3, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:35.576Z", - "updatedAt": "2021-05-30T16:11:48.431Z", + "createdAt": "2021-05-30T11:48:14.320Z", + "updatedAt": "2021-05-30T16:11:35.512Z", "payments": [] }, { - "id": "789b83d2-3278-4c90-a5c1-3aa96e61db4b", - "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", - "userHandle": "ApolloChang", - "projectId": 17363, - "startDate": "2021-01-03", - "endDate": "2021-01-09", + "id": "8252cd13-4351-4a86-9315-521963f329f5", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 181.55, - "customerRate": 97.01, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:35.600Z", - "updatedAt": "2021-05-30T16:04:27.884Z", + "createdAt": "2021-05-30T11:48:14.322Z", + "updatedAt": "2021-05-30T16:04:23.465Z", "payments": [] }, { - "id": "764fec45-842e-4d06-b009-29ba4c9c116c", - "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", - "userHandle": "ApolloChang", - "projectId": 17363, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "259d8de1-1c56-49d0-936d-d5fcbdcc5a8a", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 197.07, - "customerRate": 39.93, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:35.588Z", - "updatedAt": "2021-05-30T16:04:32.547Z", + "createdAt": "2021-05-30T11:48:14.314Z", + "updatedAt": "2021-05-30T16:05:34.486Z", "payments": [] }, { - "id": "00fa978f-e2c4-4cab-8da9-e6b0dce80258", - "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", - "userHandle": "ApolloChang", - "projectId": 17363, + "id": "1dd649b8-f536-4285-989d-56c45f1fca4d", + "resourceBookingId": "f667a667-6026-4d93-89bb-358aced982e5", + "userHandle": "GunaK-TopCoder", + "projectId": 16870, "startDate": "2021-01-17", "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 34.07, - "customerRate": 287.29, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:35.585Z", - "updatedAt": "2021-05-30T16:06:04.649Z", - "payments": [] - }, - { - "id": "daa54d7d-84f5-4f62-b58c-4c09ec26dd1a", - "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", - "userHandle": "ApolloChang", - "projectId": 17363, - "startDate": "2021-01-31", - "endDate": "2021-02-06", - "daysWorked": 1, - "memberRate": 297.19, - "customerRate": 32.3, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:35.581Z", - "updatedAt": "2021-05-30T16:11:48.432Z", + "createdAt": "2021-05-30T11:48:14.325Z", + "updatedAt": "2021-05-30T16:05:45.949Z", "payments": [] } ] }, { - "id": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "id": "d84082de-9a09-4e9b-b5ab-4024f67687c5", "projectId": 17103, - "userId": "d8e11333-af08-4149-a270-b355001b44e7", + "userId": "fa5f4dc4-2992-4066-b4cc-16ceb5d1c1b7", "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", "status": "placed", - "startDate": "2021-01-01", - "endDate": "2021-02-01", - "memberRate": 240.84, - "customerRate": 100.25, - "rateType": "weekly", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 287.14, + "customerRate": 258.37, + "rateType": "daily", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-01T12:20:21.613Z", - "updatedAt": "2021-05-30T11:48:28.143Z", + "createdAt": "2021-05-02T06:00:42.366Z", + "updatedAt": "2021-05-30T11:48:23.808Z", "workPeriods": [ { - "id": "d0571318-a0be-4263-bfe9-eba783ba8957", - "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", - "userHandle": "mvarlie", + "id": "b60da405-cee8-41c9-919b-98d9acfd9f74", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", + "projectId": 17103, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:24.607Z", + "updatedAt": "2021-05-30T16:03:52.385Z", + "payments": [] + }, + { + "id": "19110441-201e-449b-a350-661c50fb1387", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", "projectId": 17103, "startDate": "2021-01-24", "endDate": "2021-01-30", "daysWorked": 4, - "memberRate": 205.68, - "customerRate": 154.12, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:28.918Z", - "updatedAt": "2021-05-30T16:03:25.073Z", + "createdAt": "2021-05-30T11:48:24.639Z", + "updatedAt": "2021-05-30T16:05:49.510Z", "payments": [] }, { - "id": "9fd57023-518e-42e3-a998-6098ae49a3fb", - "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", - "userHandle": "mvarlie", + "id": "1f12ada9-d0e6-43df-abe5-8a78850b20b4", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", "projectId": 17103, - "startDate": "2021-01-17", - "endDate": "2021-01-23", + "startDate": "2021-02-07", + "endDate": "2021-02-13", "daysWorked": 4, - "memberRate": 202.33, - "customerRate": 1.49, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:28.905Z", - "updatedAt": "2021-05-30T16:04:11.041Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:24.601Z", + "updatedAt": "2021-05-30T16:11:41.938Z", "payments": [] }, { - "id": "b473e949-0a6e-452c-b626-6cc79ee61d1c", - "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", - "userHandle": "mvarlie", + "id": "1e94f892-ae25-4233-b9b0-81aa70c00b1e", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", "projectId": 17103, "startDate": "2021-01-10", "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 146.28, - "customerRate": 145.74, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:28.924Z", - "updatedAt": "2021-05-30T16:03:54.140Z", + "createdAt": "2021-05-30T11:48:24.604Z", + "updatedAt": "2021-05-30T16:05:45.069Z", "payments": [] }, { - "id": "b952458d-12a9-4398-b549-3fe44d3814dd", - "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", - "userHandle": "mvarlie", + "id": "4d7f0350-b2f0-4f31-acc3-6555c3756fdd", + "resourceBookingId": "d84082de-9a09-4e9b-b5ab-4024f67687c5", + "userHandle": "rtuthaya", "projectId": 17103, "startDate": "2021-01-31", "endDate": "2021-02-06", - "daysWorked": 1, - "memberRate": 214.34, - "customerRate": 167.08, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:28.922Z", - "updatedAt": "2021-05-30T16:11:44.684Z", - "payments": [] - }, - { - "id": "f206743e-f797-40a8-8dee-634fb0f08e08", - "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", - "userHandle": "mvarlie", - "projectId": 17103, - "startDate": "2021-01-03", - "endDate": "2021-01-09", "daysWorked": 5, - "memberRate": 184.32, - "customerRate": 255.03, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:28.873Z", - "updatedAt": "2021-05-30T16:02:57.423Z", - "payments": [] - }, - { - "id": "ef0f5c89-c979-4504-9b20-54b110a1495a", - "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", - "userHandle": "mvarlie", - "projectId": 17103, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 1, - "memberRate": 166.73, - "customerRate": 189.61, - "paymentStatus": "partially-completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:28.915Z", - "updatedAt": "2021-05-30T16:11:44.683Z", + "createdAt": "2021-05-30T11:48:24.637Z", + "updatedAt": "2021-05-30T16:05:08.370Z", "payments": [] } ] }, { - "id": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", - "projectId": 16899, - "userId": "47034de0-698d-4e1b-a10b-ae4b8c59288e", - "jobId": "fe270791-bc24-4f6a-8c1b-b897f5d97d2f", + "id": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "projectId": 16870, + "userId": "60d3e956-820b-4d59-a30b-9309b838fac5", + "jobId": "fe539bef-9119-4a8c-b7b0-915e7e3a3ba3", "status": "placed", "startDate": "2021-01-01", "endDate": "2021-02-01", - "memberRate": 4.07, - "customerRate": 258.37, - "rateType": "monthly", + "memberRate": 60.63, + "customerRate": 132.43, + "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-01T09:36:02.161Z", - "updatedAt": "2021-05-30T11:48:31.516Z", + "createdAt": "2021-04-30T08:09:51.618Z", + "updatedAt": "2021-05-30T11:48:20.311Z", "workPeriods": [ { - "id": "0ae1abc8-3e8d-4eea-933e-ab3cfa7c6826", - "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", - "userHandle": "brijgogogo", - "projectId": 16899, - "startDate": "2021-01-31", - "endDate": "2021-02-06", + "id": "6ad492cf-4d90-4608-8dd5-13aaafad12e2", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2020-12-27", + "endDate": "2021-01-02", "daysWorked": 1, - "memberRate": 122.15, - "customerRate": 25.64, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:32.269Z", - "updatedAt": "2021-05-30T16:11:46.620Z", + "createdAt": "2021-05-30T11:48:21.194Z", + "updatedAt": "2021-05-30T16:11:39.976Z", "payments": [] }, { - "id": "97fd7b67-0cd7-47fb-9017-7a7b653ef940", - "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", - "userHandle": "brijgogogo", - "projectId": 16899, + "id": "61134c36-5e69-469c-bc50-75648f7949ca", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, "startDate": "2021-01-10", "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 298.22, - "customerRate": 271.9, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:32.256Z", - "updatedAt": "2021-05-30T16:04:14.644Z", + "createdAt": "2021-05-30T11:48:21.187Z", + "updatedAt": "2021-05-30T16:04:51.659Z", "payments": [] }, { - "id": "3fe04a0d-62ff-4060-935a-4f294ee19fac", - "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", - "userHandle": "brijgogogo", - "projectId": 16899, - "startDate": "2020-12-27", - "endDate": "2021-01-02", + "id": "f095424c-9a15-4f37-b8e4-1cd685f17451", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", "daysWorked": 1, - "memberRate": 280.11, - "customerRate": 195.99, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:32.237Z", - "updatedAt": "2021-05-30T16:11:46.621Z", + "createdAt": "2021-05-30T11:48:21.198Z", + "updatedAt": "2021-05-30T16:11:39.977Z", "payments": [] }, { - "id": "a3daf653-c87c-451b-acfc-508ef8364fbb", - "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", - "userHandle": "brijgogogo", - "projectId": 16899, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "f79b0a83-5f72-4e9c-bffe-4ebf694db7f4", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 207.81, - "customerRate": 297.59, - "paymentStatus": "cancelled", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:32.273Z", - "updatedAt": "2021-05-30T16:04:05.668Z", + "createdAt": "2021-05-30T11:48:21.107Z", + "updatedAt": "2021-05-30T16:02:50.987Z", "payments": [] }, { - "id": "15337715-0c4a-4036-a1d3-5aaa14a214af", - "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", - "userHandle": "brijgogogo", - "projectId": 16899, + "id": "c75ee2eb-a7c0-4eb8-83f9-860ce22d1b03", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, "startDate": "2021-01-03", "endDate": "2021-01-09", "daysWorked": 5, - "memberRate": 104.85, - "customerRate": 260.77, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:32.252Z", - "updatedAt": "2021-05-30T16:05:51.388Z", + "createdAt": "2021-05-30T11:48:21.104Z", + "updatedAt": "2021-05-30T16:03:34.616Z", "payments": [] }, { - "id": "304a0ba1-2534-46c0-b2e9-9d85936755df", - "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", - "userHandle": "brijgogogo", - "projectId": 16899, - "startDate": "2021-01-17", - "endDate": "2021-01-23", + "id": "b0aad7c5-bafb-4cae-90ca-832334505e9b", + "resourceBookingId": "e673b52e-738d-47f9-bf37-68f6b5ed1926", + "userHandle": "Hypernova", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 129.72, - "customerRate": 93.25, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:32.234Z", - "updatedAt": "2021-05-30T16:05:28.904Z", + "createdAt": "2021-05-30T11:48:21.192Z", + "updatedAt": "2021-05-30T16:03:58.410Z", "payments": [] } ] }, { - "id": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", - "projectId": 16706, - "userId": "149c9ad0-f5d7-4192-8c61-f634f6120816", - "jobId": "fc2b006d-997b-49c3-a414-59ee54a48f9f", - "status": "sourcing", + "id": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "projectId": 16899, + "userId": "5bc40e16-4fdb-40f1-93fe-de465789e1b2", + "jobId": "fe270791-bc24-4f6a-8c1b-b897f5d97d2f", + "status": "placed", "startDate": "2021-01-11", "endDate": "2021-02-11", - "memberRate": 201.77, - "customerRate": 84.88, + "memberRate": 271.93, + "customerRate": 102.37, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-26T08:27:38.944Z", - "updatedAt": "2021-05-30T11:41:01.828Z", + "createdAt": "2021-05-01T09:45:15.939Z", + "updatedAt": "2021-05-30T11:48:39.054Z", "workPeriods": [ { - "id": "b21eed94-dce5-42e1-9734-155594773222", - "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", - "userHandle": "lt_dan", - "projectId": 16706, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "eeea5cf0-513c-4d1a-9318-a376aa86c28f", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, + "startDate": "2021-01-17", + "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 186.09, - "customerRate": 37.15, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:41:02.603Z", - "updatedAt": "2021-05-30T16:03:56.759Z", + "createdAt": "2021-05-30T11:48:39.879Z", + "updatedAt": "2021-05-30T16:03:01.733Z", "payments": [] }, { - "id": "85201917-77e0-41d1-8c76-66eefcbc23e0", - "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", - "userHandle": "lt_dan", - "projectId": 16706, + "id": "ab2c5ad4-165f-48d2-bf1c-005b56e049ce", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, "startDate": "2021-02-07", "endDate": "2021-02-13", "daysWorked": 4, - "memberRate": 143.95, - "customerRate": 7.02, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:41:02.617Z", - "updatedAt": "2021-05-30T16:11:54.733Z", + "createdAt": "2021-05-30T11:48:39.824Z", + "updatedAt": "2021-05-30T16:11:51.073Z", "payments": [] }, { - "id": "56105e62-d690-484c-9fe8-b6d18671c9ac", - "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", - "userHandle": "lt_dan", - "projectId": 16706, - "startDate": "2021-01-10", - "endDate": "2021-01-16", + "id": "aed553d8-4eb4-45a8-86f0-3c21f81c7570", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, + "startDate": "2021-01-24", + "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 127.23, - "customerRate": 162.5, - "paymentStatus": "completed", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:41:02.600Z", - "updatedAt": "2021-05-30T16:04:59.664Z", + "createdAt": "2021-05-30T11:48:39.870Z", + "updatedAt": "2021-05-30T16:04:00.297Z", "payments": [] }, { - "id": "5d6c9cf8-a6bc-43b9-af35-67812fc10db9", - "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", - "userHandle": "lt_dan", - "projectId": 16706, - "startDate": "2021-01-31", - "endDate": "2021-02-06", + "id": "4c9a7d50-8014-4ff2-9867-98dc66e466ac", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 140.77, - "customerRate": 161.96, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:41:02.595Z", - "updatedAt": "2021-05-30T16:04:54.450Z", + "createdAt": "2021-05-30T11:48:39.838Z", + "updatedAt": "2021-05-30T16:05:09.308Z", "payments": [] }, { - "id": "34ab1933-c9a3-4087-b220-3716d3729703", - "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", - "userHandle": "lt_dan", - "projectId": 16706, - "startDate": "2021-01-17", - "endDate": "2021-01-23", + "id": "d5c4716f-e26a-4d3f-a57d-2410d7537ecd", + "resourceBookingId": "9e6e2bd4-1e4d-401b-871b-2c8fe8f44b54", + "userHandle": "ramag", + "projectId": 16899, + "startDate": "2021-01-31", + "endDate": "2021-02-06", "daysWorked": 5, - "memberRate": 202.33, - "customerRate": 140.66, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:41:02.645Z", - "updatedAt": "2021-05-30T16:05:24.577Z", + "createdAt": "2021-05-30T11:48:39.820Z", + "updatedAt": "2021-05-30T16:03:21.454Z", "payments": [] } ] }, { - "id": "9be5f15b-2114-4e35-8762-137e1d7b3740", - "projectId": 16714, - "userId": "bde2cf99-b290-40cd-a064-9b6bb7e54bea", - "jobId": "fc0240f0-8c8f-40ce-a551-e83b45673098", - "status": "sourcing", - "startDate": "2021-01-11", - "endDate": "2021-02-11", - "memberRate": 115.29, - "customerRate": 84.88, + "id": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "projectId": 17290, + "userId": "0eaf032f-f376-47cc-b7aa-668685efac90", + "jobId": "fe600350-0a6d-4dac-922f-a6a7d285daa1", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 106.51, + "customerRate": 111.21, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-26T08:16:00.294Z", - "updatedAt": "2021-05-30T11:48:40.735Z", + "createdAt": "2021-05-18T08:18:13.123Z", + "updatedAt": "2021-05-30T11:48:52.915Z", "workPeriods": [ { - "id": "ae316c25-ec04-4bc1-b209-798b69ed5250", - "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", - "userHandle": "atish.chandra", - "projectId": 16714, - "startDate": "2021-01-31", - "endDate": "2021-02-06", + "id": "e6b70714-8bd6-47ce-9e58-f04d3c25ee28", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2021-01-24", + "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 34.07, - "customerRate": 55.77, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:41.592Z", - "updatedAt": "2021-05-30T16:04:01.125Z", + "createdAt": "2021-05-30T11:48:53.719Z", + "updatedAt": "2021-05-30T16:03:09.841Z", "payments": [] }, { - "id": "7501c035-46c4-4704-973a-595145bd2d17", - "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", - "userHandle": "atish.chandra", - "projectId": 16714, - "startDate": "2021-02-07", - "endDate": "2021-02-13", - "daysWorked": 4, - "memberRate": 114.76, - "customerRate": 268.61, + "id": "b6c1f079-e5cb-46a0-a6bf-5988ec013c4c", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:53.776Z", + "updatedAt": "2021-05-30T16:03:49.745Z", + "payments": [] + }, + { + "id": "064d2511-9af6-4d6a-be4f-79eebacc6345", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:41.588Z", - "updatedAt": "2021-05-30T16:11:52.009Z", + "createdAt": "2021-05-30T11:48:53.778Z", + "updatedAt": "2021-05-30T16:11:59.550Z", "payments": [] }, { - "id": "519b4818-8e39-4d4c-bcc6-3486816a4bc8", - "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", - "userHandle": "atish.chandra", - "projectId": 16714, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": 5, - "memberRate": 173.56, - "customerRate": 140.66, + "id": "c8c69543-1598-43e4-9ef6-8a569ebdf831", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:41.595Z", - "updatedAt": "2021-05-30T16:05:04.624Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:53.716Z", + "updatedAt": "2021-05-30T16:11:59.551Z", "payments": [] }, { - "id": "13444e2f-1254-40fc-a7d9-08ca49fc7239", - "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", - "userHandle": "atish.chandra", - "projectId": 16714, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "0d237fa9-3fe9-48dc-82b8-7027edddc5a1", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 276.5, - "customerRate": 287.29, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:41.597Z", - "updatedAt": "2021-05-30T16:05:53.017Z", + "createdAt": "2021-05-30T11:48:53.731Z", + "updatedAt": "2021-05-30T16:05:57.480Z", "payments": [] }, { - "id": "66c66f68-3882-4521-9c4e-ce08e6b47ca0", - "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", - "userHandle": "atish.chandra", - "projectId": 16714, + "id": "7bc96a71-deda-4cde-b8b0-f809cea1398a", + "resourceBookingId": "72829b1f-9183-4660-815f-d3e80d38a5a9", + "userHandle": "gliu", + "projectId": 17290, "startDate": "2021-01-17", "endDate": "2021-01-23", - "daysWorked": 4, - "memberRate": 43.3, - "customerRate": 72.49, + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:41.579Z", - "updatedAt": "2021-05-30T16:04:44.142Z", + "createdAt": "2021-05-30T11:48:53.710Z", + "updatedAt": "2021-05-30T16:04:26.082Z", "payments": [] } ] }, { - "id": "80693c90-7714-47ac-b8d9-b1c93aed910f", - "projectId": 16805, - "userId": "13330208-ab10-4ca3-9fd1-a132fbf7ac4e", - "jobId": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "id": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "projectId": 17290, + "userId": "1f6ca39c-0620-4de0-9bb2-d64d4ce26b42", + "jobId": "fe600350-0a6d-4dac-922f-a6a7d285daa1", "status": "placed", - "startDate": "2021-01-01", - "endDate": "2021-02-01", - "memberRate": 4.79, - "customerRate": 146.2, + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 4.07, + "customerRate": 296.66, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-19T11:04:22.566Z", - "updatedAt": "2021-05-30T11:48:46.861Z", + "createdAt": "2021-05-18T07:05:53.664Z", + "updatedAt": "2021-05-30T11:48:33.177Z", "workPeriods": [ { - "id": "f2a85b87-8e75-4db9-b29c-a3c170101ed6", - "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", - "userHandle": "Soumyajit_Lotus", - "projectId": 16805, + "id": "e29735b3-cf81-4877-8fd5-6d346a1824f0", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, "startDate": "2021-01-17", "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 104.6, - "customerRate": 217.32, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:47.631Z", - "updatedAt": "2021-05-30T16:02:54.605Z", - "payments": [] - }, - { - "id": "e6801711-327e-4387-a7f0-d592b49b1ba3", - "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", - "userHandle": "Soumyajit_Lotus", - "projectId": 16805, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": 5, - "memberRate": 54.82, - "customerRate": 191.42, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:47.726Z", - "updatedAt": "2021-05-30T16:03:10.690Z", - "payments": [] - }, - { - "id": "cd44a405-c77a-4f6d-95a7-c10de740eb29", - "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", - "userHandle": "Soumyajit_Lotus", - "projectId": 16805, - "startDate": "2021-01-03", - "endDate": "2021-01-09", + "createdAt": "2021-05-30T11:48:33.942Z", + "updatedAt": "2021-05-30T16:03:12.511Z", + "payments": [] + }, + { + "id": "df0b3604-7ee9-4862-a8d1-abe8a2142f77", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, + "startDate": "2021-01-31", + "endDate": "2021-02-06", "daysWorked": 5, - "memberRate": 70.98, - "customerRate": 107.72, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:47.728Z", - "updatedAt": "2021-05-30T16:03:27.679Z", + "createdAt": "2021-05-30T11:48:33.954Z", + "updatedAt": "2021-05-30T16:03:15.193Z", "payments": [] }, { - "id": "6565d172-61c4-4357-a01f-b8e6bf179e2f", - "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", - "userHandle": "Soumyajit_Lotus", - "projectId": 16805, - "startDate": "2021-01-31", - "endDate": "2021-02-06", - "daysWorked": 1, - "memberRate": 172.94, - "customerRate": 67.44, + "id": "9a480e44-2026-4327-a220-715ace30743e", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:47.638Z", - "updatedAt": "2021-05-30T16:11:55.746Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:33.952Z", + "updatedAt": "2021-05-30T16:04:12.811Z", "payments": [] }, { - "id": "f7c8a1e8-76c6-4eee-bb99-15cce5cb8b1f", - "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", - "userHandle": "Soumyajit_Lotus", - "projectId": 16805, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 1, - "memberRate": 219.84, - "customerRate": 122.72, + "id": "b6147962-6666-4534-8b73-0c7f9a7052e8", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:47.648Z", - "updatedAt": "2021-05-30T16:11:55.747Z", + "createdAt": "2021-05-30T11:48:33.959Z", + "updatedAt": "2021-05-30T16:11:47.528Z", "payments": [] }, { - "id": "a71105ef-a2b8-4163-b2de-9ddcaa8329bb", - "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", - "userHandle": "Soumyajit_Lotus", - "projectId": 16805, + "id": "47ac2474-d9e9-416d-afa8-fea8fb4f2a6c", + "resourceBookingId": "c666e835-4145-406e-b6bb-8b0f98ed8f68", + "userHandle": "suacoustic", + "projectId": 17290, "startDate": "2021-01-24", "endDate": "2021-01-30", "daysWorked": 4, - "memberRate": 252.02, - "customerRate": 219.79, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:47.731Z", - "updatedAt": "2021-05-30T16:04:02.955Z", + "createdAt": "2021-05-30T11:48:33.940Z", + "updatedAt": "2021-05-30T16:05:12.030Z", "payments": [] } ] }, { - "id": "87db42ad-f3fa-4325-99f4-d5ac6e938219", - "projectId": 17232, - "userId": "8e6bfd51-fd78-45fa-9234-172976168f29", - "jobId": "ff76b81d-f49b-4019-b50e-c7932a818f19", + "id": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "projectId": 16781, + "userId": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "jobId": "ff3feeae-d4f7-457c-bff7-215be5efe2b8", "status": "placed", - "startDate": "2021-01-11", - "endDate": "2021-02-11", - "memberRate": 225.34, - "customerRate": 170.64, - "rateType": "monthly", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 54.02, + "customerRate": 217.99, + "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-18T04:01:23.725Z", - "updatedAt": "2021-05-30T11:48:44.244Z", + "createdAt": "2021-01-12T15:02:15.210Z", + "updatedAt": "2021-05-30T11:48:59.149Z", "workPeriods": [ { - "id": "eeda07cc-8a10-4337-ab8d-e1107e4072a0", - "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", - "userHandle": "cyber-guard", - "projectId": 17232, - "startDate": "2021-01-10", - "endDate": "2021-01-16", + "id": "d062e2fe-2446-4fb5-b0b3-0577cd57fd7c", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2021-01-17", + "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 204.06, - "customerRate": 143.1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:45.002Z", - "updatedAt": "2021-05-30T16:03:02.635Z", + "createdAt": "2021-05-30T11:48:59.909Z", + "updatedAt": "2021-05-30T16:03:24.198Z", "payments": [] }, { - "id": "7aaa313f-62be-4483-80b2-87a95b4a0b8c", - "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", - "userHandle": "cyber-guard", - "projectId": 17232, + "id": "bdd60068-9e8d-4c43-8440-48a066ba4396", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, "startDate": "2021-01-31", "endDate": "2021-02-06", - "daysWorked": 5, - "memberRate": 185.82, - "customerRate": 286.29, + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:45.006Z", - "updatedAt": "2021-05-30T16:04:26.995Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:59.921Z", + "updatedAt": "2021-05-30T16:12:03.178Z", "payments": [] }, { - "id": "c0568bd9-1523-494d-bb3b-4b7a89026de9", - "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", - "userHandle": "cyber-guard", - "projectId": 17232, - "startDate": "2021-02-07", - "endDate": "2021-02-13", - "daysWorked": 4, - "memberRate": 248.86, - "customerRate": 271.75, + "id": "a0eec246-3b4d-41b6-9d54-b892513bd727", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:45.074Z", - "updatedAt": "2021-05-30T16:11:53.758Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:00.007Z", + "updatedAt": "2021-05-30T16:04:10.173Z", "payments": [] }, { - "id": "4f7b2806-cdc4-40de-979e-82573123b1ce", - "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", - "userHandle": "cyber-guard", - "projectId": 17232, + "id": "752f3cf3-a9c2-487c-93ae-22d21af10403", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, "startDate": "2021-01-24", "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 162.88, - "customerRate": 11.72, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:45.076Z", - "updatedAt": "2021-05-30T16:05:06.523Z", + "createdAt": "2021-05-30T11:49:00.041Z", + "updatedAt": "2021-05-30T16:04:34.403Z", "payments": [] }, { - "id": "3a652bb3-69b2-4e9b-b6e7-804568a9a76b", - "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", - "userHandle": "cyber-guard", - "projectId": 17232, - "startDate": "2021-01-17", - "endDate": "2021-01-23", + "id": "5be5a577-32cd-4557-8d11-bb5723dd7be2", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2021-01-03", + "endDate": "2021-01-09", "daysWorked": 5, - "memberRate": 54.97, - "customerRate": 167.68, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:45.012Z", - "updatedAt": "2021-05-30T16:05:20.251Z", + "createdAt": "2021-05-30T11:48:59.998Z", + "updatedAt": "2021-05-30T16:04:57.065Z", + "payments": [] + }, + { + "id": "e9ff7b7f-05ec-45f9-a09e-b2c24017b59b", + "resourceBookingId": "5b93498d-eecf-4798-ad62-0dea8b4aa49e", + "userHandle": "nkumartest", + "projectId": 16781, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:59.974Z", + "updatedAt": "2021-05-30T16:12:03.180Z", "payments": [] } ] }, { - "id": "905654a2-e07d-47a3-b577-c03d100bc94a", - "projectId": 17300, - "userId": "6719d9dc-beca-4731-a4be-a214152ccadf", - "jobId": "fd13ad99-f16a-4362-9274-80f5f38895c3", + "id": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "projectId": 17363, + "userId": "c40cdb0a-4fac-4ca1-8052-d92001858887", + "jobId": "fd48d96e-b0f2-43b7-8a48-f4fa194d6bc8", "status": "placed", - "startDate": "2021-01-11", - "endDate": "2021-02-11", - "memberRate": 66, - "customerRate": 132.43, - "rateType": "daily", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 235.26, + "customerRate": 84.88, + "rateType": "hourly", "billingAccountId": 80000071, - "createdBy": "00000000-0000-0000-0000-000000000000", + "createdBy": "71c5e6a8-51d9-4fb5-91ce-d974642531af", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-19T07:40:44.211Z", - "updatedAt": "2021-05-30T11:48:42.523Z", + "createdAt": "2021-05-28T04:41:39.728Z", + "updatedAt": "2021-05-30T11:48:34.793Z", "workPeriods": [ { - "id": "6ca0c12a-eecb-4d12-b40d-89a19f2c38ad", - "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", - "userHandle": "vimal123", - "projectId": 17300, - "startDate": "2021-02-07", - "endDate": "2021-02-13", - "daysWorked": 4, - "memberRate": 216.67, - "customerRate": 281.91, + "id": "2570737c-02b4-4b90-b692-45dcc5774215", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:43.331Z", - "updatedAt": "2021-05-30T16:11:52.856Z", + "createdAt": "2021-05-30T11:48:35.576Z", + "updatedAt": "2021-05-30T16:11:48.431Z", "payments": [] }, { - "id": "92f9e88b-bc75-4934-9c3f-cdb3a846e545", - "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", - "userHandle": "vimal123", - "projectId": 17300, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "789b83d2-3278-4c90-a5c1-3aa96e61db4b", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2021-01-03", + "endDate": "2021-01-09", "daysWorked": 5, - "memberRate": 85.45, - "customerRate": 273.89, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:43.339Z", - "updatedAt": "2021-05-30T16:04:18.164Z", + "createdAt": "2021-05-30T11:48:35.600Z", + "updatedAt": "2021-05-30T16:04:27.884Z", "payments": [] }, { - "id": "bc7af7e8-873a-4ddf-a3a5-8b5f0db41827", - "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", - "userHandle": "vimal123", - "projectId": 17300, - "startDate": "2021-01-31", - "endDate": "2021-02-06", - "daysWorked": null, - "memberRate": 239.68, - "customerRate": 180.76, - "paymentStatus": "partially-completed", + "id": "764fec45-842e-4d06-b009-29ba4c9c116c", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:43.254Z", - "updatedAt": "2021-05-30T16:03:45.211Z", + "createdAt": "2021-05-30T11:48:35.588Z", + "updatedAt": "2021-05-30T16:04:32.547Z", "payments": [] }, { - "id": "11084a75-bae7-4c93-a2fc-44ff691b6ded", - "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", - "userHandle": "vimal123", - "projectId": 17300, + "id": "00fa978f-e2c4-4cab-8da9-e6b0dce80258", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, "startDate": "2021-01-17", "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 79.89, - "customerRate": 174.2, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:43.335Z", - "updatedAt": "2021-05-30T16:05:54.898Z", + "createdAt": "2021-05-30T11:48:35.585Z", + "updatedAt": "2021-05-30T16:06:04.649Z", "payments": [] }, { - "id": "6c3a492a-d2bf-4740-91b0-0d877b044268", - "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", - "userHandle": "vimal123", - "projectId": 17300, + "id": "daa54d7d-84f5-4f62-b58c-4c09ec26dd1a", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:35.581Z", + "updatedAt": "2021-05-30T16:11:48.432Z", + "payments": [] + }, + { + "id": "94a897e9-6291-4206-a0b1-74c35ff06a6e", + "resourceBookingId": "a74df62a-dba0-4214-8f8e-5e071f359afe", + "userHandle": "ApolloChang", + "projectId": 17363, "startDate": "2021-01-10", "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 66.85, - "customerRate": 154.65, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:43.275Z", - "updatedAt": "2021-05-30T16:04:41.476Z", + "createdAt": "2021-05-30T11:48:35.596Z", + "updatedAt": "2021-05-30T16:04:17.285Z", "payments": [] } ] }, { - "id": "72db31b8-f05c-497c-9bc6-b9f6692569a0", - "projectId": 16718, - "userId": "a953dce3-8dd3-413f-b253-0ca76ff59f36", - "jobId": "fb2f5f9b-5874-4dcd-af94-727fc0409760", - "status": "sourcing", - "startDate": "2021-01-11", - "endDate": "2021-02-11", - "memberRate": 269.46, - "customerRate": 138.32, - "rateType": "monthly", + "id": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "projectId": 17103, + "userId": "d8e11333-af08-4149-a270-b355001b44e7", + "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 240.84, + "customerRate": 100.25, + "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-26T08:24:56.376Z", - "updatedAt": "2021-05-30T11:48:51.233Z", + "createdAt": "2021-05-01T12:20:21.613Z", + "updatedAt": "2021-05-30T11:48:28.143Z", "workPeriods": [ { - "id": "0ed78625-e5f0-4f05-b326-632a002d150a", - "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", - "userHandle": "centurionme", - "projectId": 16718, - "startDate": "2021-02-07", - "endDate": "2021-02-13", + "id": "d0571318-a0be-4263-bfe9-eba783ba8957", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2021-01-24", + "endDate": "2021-01-30", "daysWorked": 4, - "memberRate": 46.62, - "customerRate": 76.96, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:52.006Z", - "updatedAt": "2021-05-30T16:11:58.578Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:28.918Z", + "updatedAt": "2021-05-30T16:03:25.073Z", "payments": [] }, { - "id": "279989ed-1e9e-45e9-bdb8-26c03d396344", - "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", - "userHandle": "centurionme", - "projectId": 16718, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "9fd57023-518e-42e3-a998-6098ae49a3fb", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2021-01-17", + "endDate": "2021-01-23", "daysWorked": 4, - "memberRate": 173.64, - "customerRate": 160.37, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:51.981Z", - "updatedAt": "2021-05-30T16:05:33.514Z", + "createdAt": "2021-05-30T11:48:28.905Z", + "updatedAt": "2021-05-30T16:04:11.041Z", "payments": [] }, { - "id": "31530732-4c7a-429c-9384-c219f16590fa", - "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", - "userHandle": "centurionme", - "projectId": 16718, + "id": "b473e949-0a6e-452c-b626-6cc79ee61d1c", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, "startDate": "2021-01-10", "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 239.68, - "customerRate": 84.09, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:51.988Z", - "updatedAt": "2021-05-30T16:05:27.123Z", + "createdAt": "2021-05-30T11:48:28.924Z", + "updatedAt": "2021-05-30T16:03:54.140Z", "payments": [] }, { - "id": "91065330-b979-4b3c-b084-0e14b6be6740", - "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", - "userHandle": "centurionme", - "projectId": 16718, + "id": "b952458d-12a9-4398-b549-3fe44d3814dd", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, "startDate": "2021-01-31", "endDate": "2021-02-06", - "daysWorked": 5, - "memberRate": 191.95, - "customerRate": 260.09, + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:51.984Z", - "updatedAt": "2021-05-30T16:04:19.946Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:28.922Z", + "updatedAt": "2021-05-30T16:11:44.684Z", "payments": [] }, { - "id": "06187a37-d29a-4bdb-bcb1-e0e7f57eec4a", - "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", - "userHandle": "centurionme", - "projectId": 16718, - "startDate": "2021-01-17", - "endDate": "2021-01-23", + "id": "f206743e-f797-40a8-8dee-634fb0f08e08", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2021-01-03", + "endDate": "2021-01-09", "daysWorked": 5, - "memberRate": 124.66, - "customerRate": 219.67, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:51.978Z", - "updatedAt": "2021-05-30T16:06:00.996Z", + "createdAt": "2021-05-30T11:48:28.873Z", + "updatedAt": "2021-05-30T16:02:57.423Z", + "payments": [] + }, + { + "id": "ef0f5c89-c979-4504-9b20-54b110a1495a", + "resourceBookingId": "caf8fde9-2137-48fb-b388-24a1801eacf3", + "userHandle": "mvarlie", + "projectId": 17103, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:28.915Z", + "updatedAt": "2021-05-30T16:11:44.683Z", "payments": [] } ] }, { - "id": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", - "projectId": 16870, - "userId": "cc959274-bb53-4612-a4f4-af62496b026c", - "jobId": "fe8da845-5313-496f-b859-9824bd06a0db", + "id": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "projectId": 16899, + "userId": "47034de0-698d-4e1b-a10b-ae4b8c59288e", + "jobId": "fe270791-bc24-4f6a-8c1b-b897f5d97d2f", "status": "placed", - "startDate": "2021-01-12", - "endDate": "2021-02-12", - "memberRate": 240.84, - "customerRate": 188.33, - "rateType": "weekly", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 4.07, + "customerRate": 258.37, + "rateType": "monthly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-01T09:30:37.276Z", - "updatedAt": "2021-05-30T11:48:54.737Z", + "createdAt": "2021-05-01T09:36:02.161Z", + "updatedAt": "2021-05-30T11:48:31.516Z", "workPeriods": [ { - "id": "c44339a8-5562-493d-9e59-02fded34dadd", - "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", - "userHandle": "MikeKusold", - "projectId": 16870, - "startDate": "2021-01-17", - "endDate": "2021-01-23", - "daysWorked": 5, - "memberRate": 6.22, - "customerRate": 62.03, + "id": "0ae1abc8-3e8d-4eea-933e-ab3cfa7c6826", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:55.482Z", - "updatedAt": "2021-05-30T16:03:38.050Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:32.269Z", + "updatedAt": "2021-05-30T16:11:46.620Z", "payments": [] }, { - "id": "6e8627d9-f3b9-4e56-ba48-0d4cd0572beb", - "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", - "userHandle": "MikeKusold", - "projectId": 16870, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "97fd7b67-0cd7-47fb-9017-7a7b653ef940", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 21.58, - "customerRate": 72.49, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:55.537Z", - "updatedAt": "2021-05-30T16:04:39.704Z", + "createdAt": "2021-05-30T11:48:32.256Z", + "updatedAt": "2021-05-30T16:04:14.644Z", "payments": [] }, { - "id": "20b18eeb-a78f-4ff2-8a3b-fbd1cfba567c", - "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", - "userHandle": "MikeKusold", - "projectId": 16870, - "startDate": "2021-02-07", - "endDate": "2021-02-13", - "daysWorked": null, - "memberRate": 41.26, - "customerRate": 286.29, + "id": "3fe04a0d-62ff-4060-935a-4f294ee19fac", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:32.237Z", + "updatedAt": "2021-05-30T16:11:46.621Z", + "payments": [] + }, + { + "id": "a3daf653-c87c-451b-acfc-508ef8364fbb", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:55.526Z", - "updatedAt": "2021-05-30T16:05:40.643Z", + "createdAt": "2021-05-30T11:48:32.273Z", + "updatedAt": "2021-05-30T16:04:05.668Z", "payments": [] }, { - "id": "36abc507-0e01-46e3-ab78-52c0e8f848b1", - "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", - "userHandle": "MikeKusold", - "projectId": 16870, - "startDate": "2021-01-31", - "endDate": "2021-02-06", + "id": "15337715-0c4a-4036-a1d3-5aaa14a214af", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-03", + "endDate": "2021-01-09", "daysWorked": 5, - "memberRate": 102.8, - "customerRate": 149.44, - "paymentStatus": "partially-completed", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:55.539Z", - "updatedAt": "2021-05-30T16:05:22.767Z", + "createdAt": "2021-05-30T11:48:32.252Z", + "updatedAt": "2021-05-30T16:05:51.388Z", "payments": [] }, { - "id": "4b8cc238-bb26-4fbf-ab74-a86c1d9a47ce", - "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", - "userHandle": "MikeKusold", - "projectId": 16870, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": 4, - "memberRate": 96.7, - "customerRate": 13.09, - "paymentStatus": "cancelled", + "id": "304a0ba1-2534-46c0-b2e9-9d85936755df", + "resourceBookingId": "c8cb4245-83d9-4c59-b595-f032b53b2cbc", + "userHandle": "brijgogogo", + "projectId": 16899, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:55.532Z", - "updatedAt": "2021-05-30T16:12:00.505Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:32.234Z", + "updatedAt": "2021-05-30T16:05:28.904Z", "payments": [] } ] }, { - "id": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", - "projectId": 16870, - "userId": "acdf9ebe-8358-4bd3-9374-1d86cf27e5f4", - "jobId": "fe8da845-5313-496f-b859-9824bd06a0db", - "status": "placed", - "startDate": "2021-01-21", - "endDate": "2021-02-21", - "memberRate": 114.33, - "customerRate": 258.37, + "id": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "projectId": 16706, + "userId": "149c9ad0-f5d7-4192-8c61-f634f6120816", + "jobId": "fc2b006d-997b-49c3-a414-59ee54a48f9f", + "status": "sourcing", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 201.77, + "customerRate": 84.88, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-01T09:30:59.306Z", - "updatedAt": "2021-05-30T11:48:56.459Z", + "createdAt": "2021-01-26T08:27:38.944Z", + "updatedAt": "2021-05-30T11:41:01.828Z", "workPeriods": [ { - "id": "77dcf745-bd59-40d8-b563-75a4d5354d29", - "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", - "userHandle": "newwayenjoy", - "projectId": 16870, - "startDate": "2021-02-14", - "endDate": "2021-02-20", - "daysWorked": null, - "memberRate": 158.66, - "customerRate": 19.76, + "id": "b21eed94-dce5-42e1-9734-155594773222", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:57.208Z", - "updatedAt": "2021-05-30T16:04:30.745Z", + "createdAt": "2021-05-30T11:41:02.603Z", + "updatedAt": "2021-05-30T16:03:56.759Z", "payments": [] }, { - "id": "97cb1bad-772a-4f1e-a5a3-f0b19ae766f2", - "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", - "userHandle": "newwayenjoy", - "projectId": 16870, - "startDate": "2021-02-21", - "endDate": "2021-02-27", - "daysWorked": 0, - "memberRate": 69.43, - "customerRate": 22.82, - "paymentStatus": "cancelled", + "id": "85201917-77e0-41d1-8c76-66eefcbc23e0", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:57.210Z", - "updatedAt": "2021-05-30T16:12:01.358Z", + "createdAt": "2021-05-30T11:41:02.617Z", + "updatedAt": "2021-05-30T16:11:54.733Z", "payments": [] }, { - "id": "4c626a59-e591-4e7a-88cb-1d601b9b8493", - "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", - "userHandle": "newwayenjoy", - "projectId": 16870, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "56105e62-d690-484c-9fe8-b6d18671c9ac", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 10.15, - "customerRate": 213.97, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:57.302Z", - "updatedAt": "2021-05-30T16:05:10.190Z", - "payments": [] - }, - { - "id": "55b66454-3a29-4163-9d97-7ecd2e805f71", - "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", - "userHandle": "newwayenjoy", - "projectId": 16870, - "startDate": "2021-01-17", - "endDate": "2021-01-23", - "daysWorked": 2, - "memberRate": 230.66, - "customerRate": 193.93, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:57.225Z", - "updatedAt": "2021-05-30T16:12:01.359Z", + "createdAt": "2021-05-30T11:41:02.600Z", + "updatedAt": "2021-05-30T16:04:59.664Z", "payments": [] }, { - "id": "1fa1f111-6574-47b0-8d12-6832541d496c", - "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", - "userHandle": "newwayenjoy", - "projectId": 16870, + "id": "5d6c9cf8-a6bc-43b9-af35-67812fc10db9", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, "startDate": "2021-01-31", "endDate": "2021-02-06", - "daysWorked": null, - "memberRate": 104.6, - "customerRate": 62.03, + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:57.244Z", - "updatedAt": "2021-05-30T16:05:42.410Z", + "createdAt": "2021-05-30T11:41:02.595Z", + "updatedAt": "2021-05-30T16:04:54.450Z", "payments": [] }, { - "id": "5bc5686b-95b3-49d7-9c8e-50f1dfdcb82e", - "resourceBookingId": "62c3f0c9-2bf0-4f24-8647-2c802a39cbcb", - "userHandle": "newwayenjoy", - "projectId": 16870, - "startDate": "2021-02-07", - "endDate": "2021-02-13", - "daysWorked": 4, - "memberRate": 165.75, - "customerRate": 262.91, + "id": "34ab1933-c9a3-4087-b220-3716d3729703", + "resourceBookingId": "85ed4a55-1c13-45f0-bfd3-e5e0378b42ea", + "userHandle": "lt_dan", + "projectId": 16706, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:57.306Z", - "updatedAt": "2021-05-30T16:04:57.957Z", + "createdAt": "2021-05-30T11:41:02.645Z", + "updatedAt": "2021-05-30T16:05:24.577Z", "payments": [] } ] }, { - "id": "7827dee4-012a-4fd2-9fb3-5b96913121a2", - "projectId": 16762, - "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", - "jobId": "fe481d1c-cf87-49c1-9370-695f9f754041", - "status": "placed", - "startDate": "2021-01-01", - "endDate": "2021-02-01", - "memberRate": 61.33, + "id": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "projectId": 16714, + "userId": "bde2cf99-b290-40cd-a064-9b6bb7e54bea", + "jobId": "fc0240f0-8c8f-40ce-a551-e83b45673098", + "status": "sourcing", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 115.29, "customerRate": 84.88, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-12T10:53:32.373Z", - "updatedAt": "2021-05-30T11:48:49.532Z", + "createdAt": "2021-01-26T08:16:00.294Z", + "updatedAt": "2021-05-30T11:48:40.735Z", "workPeriods": [ { - "id": "a154b1fb-06d3-4cfd-97d3-0a810a1c4317", - "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", - "userHandle": "pshah_manager", - "projectId": 16762, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "ae316c25-ec04-4bc1-b209-798b69ed5250", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, + "startDate": "2021-01-31", + "endDate": "2021-02-06", "daysWorked": 5, - "memberRate": 202.33, - "customerRate": 128, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:50.279Z", - "updatedAt": "2021-05-30T16:04:09.261Z", + "createdAt": "2021-05-30T11:48:41.592Z", + "updatedAt": "2021-05-30T16:04:01.125Z", "payments": [] }, { - "id": "a35207bd-ac1d-4539-b7bb-7a923c8a6f7f", - "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", - "userHandle": "pshah_manager", - "projectId": 16762, - "startDate": "2021-01-31", - "endDate": "2021-02-06", - "daysWorked": 1, - "memberRate": 294.23, - "customerRate": 142.66, + "id": "7501c035-46c4-4704-973a-595145bd2d17", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:50.330Z", - "updatedAt": "2021-05-30T16:11:57.609Z", - "payments": [] - }, - { - "id": "3ad03850-ebe9-4227-8b30-1303b20bbd31", - "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", - "userHandle": "pshah_manager", - "projectId": 16762, - "startDate": "2021-01-03", - "endDate": "2021-01-09", - "daysWorked": 5, - "memberRate": 157.6, - "customerRate": 40.76, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:50.292Z", - "updatedAt": "2021-05-30T16:05:19.240Z", + "createdAt": "2021-05-30T11:48:41.588Z", + "updatedAt": "2021-05-30T16:11:52.009Z", "payments": [] }, { - "id": "2ea4bffd-2519-422f-8baa-a0f74b3b398b", - "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", - "userHandle": "pshah_manager", - "projectId": 16762, + "id": "519b4818-8e39-4d4c-bcc6-3486816a4bc8", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, "startDate": "2021-01-10", "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 218.59, - "customerRate": 195.92, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:50.340Z", - "updatedAt": "2021-05-30T16:05:30.731Z", + "createdAt": "2021-05-30T11:48:41.595Z", + "updatedAt": "2021-05-30T16:05:04.624Z", "payments": [] }, { - "id": "b34361ca-eb0e-47f7-86d9-3bccbb6839d5", - "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", - "userHandle": "pshah_manager", - "projectId": 16762, - "startDate": "2021-01-17", - "endDate": "2021-01-23", - "daysWorked": null, - "memberRate": 238.31, - "customerRate": 11.09, + "id": "13444e2f-1254-40fc-a7d9-08ca49fc7239", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:50.288Z", - "updatedAt": "2021-05-30T16:03:55.011Z", + "createdAt": "2021-05-30T11:48:41.597Z", + "updatedAt": "2021-05-30T16:05:53.017Z", "payments": [] }, { - "id": "e1099a6a-7c6b-465d-bb1b-517c3fbd06f1", - "resourceBookingId": "7827dee4-012a-4fd2-9fb3-5b96913121a2", - "userHandle": "pshah_manager", - "projectId": 16762, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 1, - "memberRate": 105.58, - "customerRate": 297.59, - "paymentStatus": "partially-completed", + "id": "66c66f68-3882-4521-9c4e-ce08e6b47ca0", + "resourceBookingId": "9be5f15b-2114-4e35-8762-137e1d7b3740", + "userHandle": "atish.chandra", + "projectId": 16714, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:50.297Z", - "updatedAt": "2021-05-30T16:11:57.610Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:41.579Z", + "updatedAt": "2021-05-30T16:04:44.142Z", "payments": [] } ] }, { - "id": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "id": "80693c90-7714-47ac-b8d9-b1c93aed910f", "projectId": 16805, - "userId": "3797d69c-0bf1-421e-b086-81e36ec1f929", + "userId": "13330208-ab10-4ca3-9fd1-a132fbf7ac4e", "jobId": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", "status": "placed", - "startDate": "2021-01-11", - "endDate": "2021-02-11", - "memberRate": 1.59, - "customerRate": 170.64, + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 4.79, + "customerRate": 146.2, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-28T05:15:29.662Z", - "updatedAt": "2021-05-30T11:48:37.381Z", + "createdAt": "2021-01-19T11:04:22.566Z", + "updatedAt": "2021-05-30T11:48:46.861Z", "workPeriods": [ { - "id": "33b3b539-5741-49af-a700-fa8e9bd4abba", - "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", - "userHandle": "cp185035", + "id": "f2a85b87-8e75-4db9-b29c-a3c170101ed6", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", "projectId": 16805, - "startDate": "2021-01-24", - "endDate": "2021-01-30", - "daysWorked": 4, - "memberRate": 280.11, - "customerRate": 111.64, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:38.136Z", - "updatedAt": "2021-05-30T16:05:25.424Z", + "createdAt": "2021-05-30T11:48:47.631Z", + "updatedAt": "2021-05-30T16:02:54.605Z", "payments": [] }, { - "id": "eca90916-ded1-49d5-8beb-582bba178dd9", - "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", - "userHandle": "cp185035", + "id": "e6801711-327e-4387-a7f0-d592b49b1ba3", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", "projectId": 16805, - "startDate": "2021-02-07", - "endDate": "2021-02-13", - "daysWorked": 4, - "memberRate": 65.17, - "customerRate": 4.22, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:38.215Z", - "updatedAt": "2021-05-30T16:11:50.205Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:47.726Z", + "updatedAt": "2021-05-30T16:03:10.690Z", "payments": [] }, { - "id": "2f176676-60f7-4d27-bb79-d1183eb0b7e0", - "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", - "userHandle": "cp185035", + "id": "cd44a405-c77a-4f6d-95a7-c10de740eb29", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", "projectId": 16805, - "startDate": "2021-01-17", - "endDate": "2021-01-23", + "startDate": "2021-01-03", + "endDate": "2021-01-09", "daysWorked": 5, - "memberRate": 196.76, - "customerRate": 42.45, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:38.152Z", - "updatedAt": "2021-05-30T16:05:29.869Z", + "createdAt": "2021-05-30T11:48:47.728Z", + "updatedAt": "2021-05-30T16:03:27.679Z", "payments": [] }, { - "id": "0ac738d8-03be-4ba4-a86b-2a1f65666cd5", - "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", - "userHandle": "cp185035", + "id": "6565d172-61c4-4357-a01f-b8e6bf179e2f", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", "projectId": 16805, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": 5, - "memberRate": 188.95, - "customerRate": 122.72, - "paymentStatus": "partially-completed", + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:38.220Z", - "updatedAt": "2021-05-30T16:05:59.284Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:47.638Z", + "updatedAt": "2021-05-30T16:11:55.746Z", "payments": [] }, { - "id": "b180bd57-30b3-4092-affc-c306401edd7d", - "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", - "userHandle": "cp185035", + "id": "f7c8a1e8-76c6-4eee-bb99-15cce5cb8b1f", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", "projectId": 16805, - "startDate": "2021-01-31", - "endDate": "2021-02-06", + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:47.648Z", + "updatedAt": "2021-05-30T16:11:55.747Z", + "payments": [] + }, + { + "id": "a71105ef-a2b8-4163-b2de-9ddcaa8329bb", + "resourceBookingId": "80693c90-7714-47ac-b8d9-b1c93aed910f", + "userHandle": "Soumyajit_Lotus", + "projectId": 16805, + "startDate": "2021-01-24", + "endDate": "2021-01-30", "daysWorked": 4, - "memberRate": 126.33, - "customerRate": 160.37, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:38.150Z", - "updatedAt": "2021-05-30T16:03:57.617Z", + "createdAt": "2021-05-30T11:48:47.731Z", + "updatedAt": "2021-05-30T16:04:02.955Z", "payments": [] } ] }, { - "id": "c9f268af-a03f-476e-a58b-1a2bb52324e0", - "projectId": 16739, - "userId": "3ed9015f-09d8-4173-bfcd-5dcc60c52060", - "jobId": "fc5ba131-566f-46fe-8501-79c593241896", + "id": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "projectId": 17232, + "userId": "8e6bfd51-fd78-45fa-9234-172976168f29", + "jobId": "ff76b81d-f49b-4019-b50e-c7932a818f19", "status": "placed", - "startDate": "2021-01-01", - "endDate": "2021-02-01", - "memberRate": 61.33, - "customerRate": 114.05, - "rateType": "hourly", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 225.34, + "customerRate": 170.64, + "rateType": "monthly", "billingAccountId": 80000071, - "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-17T13:41:56.896Z", - "updatedAt": "2021-05-30T11:48:29.760Z", + "createdAt": "2021-05-18T04:01:23.725Z", + "updatedAt": "2021-05-30T11:48:44.244Z", "workPeriods": [ { - "id": "7d495eed-d042-4a96-beed-dc2f2c1054c1", - "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", - "userHandle": "epicdoom", - "projectId": 16739, - "startDate": "2021-01-17", - "endDate": "2021-01-23", - "daysWorked": 5, - "memberRate": 245.61, - "customerRate": 158.61, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:30.527Z", - "updatedAt": "2021-05-30T16:04:25.202Z", - "payments": [] - }, - { - "id": "7468173d-6d92-4560-802e-6329ab656754", - "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", - "userHandle": "epicdoom", - "projectId": 16739, - "startDate": "2021-01-03", - "endDate": "2021-01-09", + "id": "eeda07cc-8a10-4337-ab8d-e1107e4072a0", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 3.94, - "customerRate": 131.83, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:30.544Z", - "updatedAt": "2021-05-30T16:04:36.153Z", + "createdAt": "2021-05-30T11:48:45.002Z", + "updatedAt": "2021-05-30T16:03:02.635Z", "payments": [] }, { - "id": "1dc38edd-3b56-4ed3-ae6c-ea2527076b32", - "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", - "userHandle": "epicdoom", - "projectId": 16739, - "startDate": "2021-01-10", - "endDate": "2021-01-16", + "id": "7aaa313f-62be-4483-80b2-87a95b4a0b8c", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, + "startDate": "2021-01-31", + "endDate": "2021-02-06", "daysWorked": 5, - "memberRate": 43.7, - "customerRate": 154.12, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:30.606Z", - "updatedAt": "2021-05-30T16:05:46.841Z", + "createdAt": "2021-05-30T11:48:45.006Z", + "updatedAt": "2021-05-30T16:04:26.995Z", "payments": [] }, { - "id": "212909c4-b1e9-4d12-b2ca-4175ccbb2d7f", - "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", - "userHandle": "epicdoom", - "projectId": 16739, - "startDate": "2021-01-31", - "endDate": "2021-02-06", - "daysWorked": 1, - "memberRate": 85.9, - "customerRate": 134.84, + "id": "c0568bd9-1523-494d-bb3b-4b7a89026de9", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:30.530Z", - "updatedAt": "2021-05-30T16:11:45.643Z", + "createdAt": "2021-05-30T11:48:45.074Z", + "updatedAt": "2021-05-30T16:11:53.758Z", "payments": [] }, { - "id": "d4dffbb9-2224-4429-9d7e-4bd9d33dba70", - "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", - "userHandle": "epicdoom", - "projectId": 16739, + "id": "4f7b2806-cdc4-40de-979e-82573123b1ce", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, "startDate": "2021-01-24", "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 4.63, - "customerRate": 152.35, - "paymentStatus": "completed", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:48:30.547Z", - "updatedAt": "2021-05-30T16:03:22.384Z", + "createdAt": "2021-05-30T11:48:45.076Z", + "updatedAt": "2021-05-30T16:05:06.523Z", "payments": [] }, { - "id": "b6b60c49-615a-4367-b644-af68485b4293", - "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", - "userHandle": "epicdoom", - "projectId": 16739, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 1, - "memberRate": 18.46, - "customerRate": 171.14, + "id": "3a652bb3-69b2-4e9b-b6e7-804568a9a76b", + "resourceBookingId": "87db42ad-f3fa-4325-99f4-d5ac6e938219", + "userHandle": "cyber-guard", + "projectId": 17232, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:48:30.540Z", - "updatedAt": "2021-05-30T16:11:45.644Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:45.012Z", + "updatedAt": "2021-05-30T16:05:20.251Z", "payments": [] } ] - }, - { - "id": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", - "projectId": 17324, - "userId": "9807980a-a9e4-4f24-a48b-311fcdbf1f47", - "jobId": "fefd2618-9b66-4431-9874-1d02d7a37d90", + }, + { + "id": "905654a2-e07d-47a3-b577-c03d100bc94a", + "projectId": 17300, + "userId": "6719d9dc-beca-4731-a4be-a214152ccadf", + "jobId": "fd13ad99-f16a-4362-9274-80f5f38895c3", "status": "placed", - "startDate": "2021-01-01", - "endDate": "2021-02-01", - "memberRate": 287.14, - "customerRate": 146.2, - "rateType": "weekly", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 66, + "customerRate": 132.43, + "rateType": "daily", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-27T05:05:05.125Z", - "updatedAt": "2021-05-30T11:49:06.302Z", + "createdAt": "2021-05-19T07:40:44.211Z", + "updatedAt": "2021-05-30T11:48:42.523Z", "workPeriods": [ { - "id": "11d7db8c-b4a9-47b0-b24a-e45d4dc5fae4", - "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", - "userHandle": "bone2", - "projectId": 17324, - "startDate": "2021-01-03", - "endDate": "2021-01-09", - "daysWorked": null, - "memberRate": 85.95, - "customerRate": 178.12, + "id": "6ca0c12a-eecb-4d12-b40d-89a19f2c38ad", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:07.103Z", - "updatedAt": "2021-05-30T16:05:53.988Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:43.331Z", + "updatedAt": "2021-05-30T16:11:52.856Z", "payments": [] }, { - "id": "d711870a-4f78-431b-b5b5-ae5157999a0c", - "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", - "userHandle": "bone2", - "projectId": 17324, - "startDate": "2021-01-17", - "endDate": "2021-01-23", + "id": "92f9e88b-bc75-4934-9c3f-cdb3a846e545", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, + "startDate": "2021-01-24", + "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 58.11, - "customerRate": 143.99, - "paymentStatus": "cancelled", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:07.137Z", - "updatedAt": "2021-05-30T16:03:20.574Z", + "createdAt": "2021-05-30T11:48:43.339Z", + "updatedAt": "2021-05-30T16:04:18.164Z", "payments": [] }, { - "id": "d0266458-f9d2-42db-a716-6f114b4a0be0", - "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", - "userHandle": "bone2", - "projectId": 17324, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "11084a75-bae7-4c93-a2fc-44ff691b6ded", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, + "startDate": "2021-01-17", + "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 36.75, - "customerRate": 269.78, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:07.095Z", - "updatedAt": "2021-05-30T16:03:25.900Z", + "createdAt": "2021-05-30T11:48:43.335Z", + "updatedAt": "2021-05-30T16:05:54.898Z", "payments": [] }, { - "id": "248de422-69c3-4c5b-8919-ba18113d0350", - "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", - "userHandle": "bone2", - "projectId": 17324, + "id": "6c3a492a-d2bf-4740-91b0-0d877b044268", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, "startDate": "2021-01-10", "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 69.67, - "customerRate": 193.93, - "paymentStatus": "partially-completed", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:07.106Z", - "updatedAt": "2021-05-30T16:05:36.306Z", + "createdAt": "2021-05-30T11:48:43.275Z", + "updatedAt": "2021-05-30T16:04:41.476Z", "payments": [] }, { - "id": "6aabe458-6e77-4fbd-9092-d811e7bbd21d", - "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", - "userHandle": "bone2", - "projectId": 17324, + "id": "bc7af7e8-873a-4ddf-a3a5-8b5f0db41827", + "resourceBookingId": "905654a2-e07d-47a3-b577-c03d100bc94a", + "userHandle": "vimal123", + "projectId": 17300, "startDate": "2021-01-31", "endDate": "2021-02-06", - "daysWorked": 1, - "memberRate": 126.28, - "customerRate": 213.99, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:07.144Z", - "updatedAt": "2021-05-30T16:12:08.558Z", - "payments": [] - }, - { - "id": "662931c7-09a4-43d9-a838-cb275296e818", - "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", - "userHandle": "bone2", - "projectId": 17324, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 1, - "memberRate": 177.96, - "customerRate": 101.95, + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:07.100Z", - "updatedAt": "2021-05-30T16:12:08.560Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:43.254Z", + "updatedAt": "2021-05-30T16:03:45.211Z", "payments": [] } ] }, { - "id": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", - "projectId": 17103, - "userId": "8fe0c1c3-e63e-4047-9854-01f03b166bd8", - "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", - "status": "closed", - "startDate": "2021-01-02", - "endDate": "2021-02-02", - "memberRate": 85.22, - "customerRate": 170.64, - "rateType": "weekly", + "id": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "projectId": 16718, + "userId": "a953dce3-8dd3-413f-b253-0ca76ff59f36", + "jobId": "fb2f5f9b-5874-4dcd-af94-727fc0409760", + "status": "sourcing", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 269.46, + "customerRate": 138.32, + "rateType": "monthly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-01T10:25:45.827Z", - "updatedAt": "2021-05-30T11:49:17.657Z", + "createdAt": "2021-01-26T08:24:56.376Z", + "updatedAt": "2021-05-30T11:48:51.233Z", "workPeriods": [ { - "id": "0466ddf6-83ba-41ee-b299-4abb2b5f8a3b", - "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", - "userHandle": "marathon_zhang", - "projectId": 17103, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 0, - "memberRate": 191.67, - "customerRate": 40.76, + "id": "0ed78625-e5f0-4f05-b326-632a002d150a", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:18.405Z", - "updatedAt": "2021-05-30T16:12:15.134Z", - "payments": [] - }, - { - "id": "bd92f07b-4b57-4486-9101-254578cf32f8", - "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", - "userHandle": "marathon_zhang", - "projectId": 17103, - "startDate": "2021-01-17", - "endDate": "2021-01-23", - "daysWorked": 5, - "memberRate": 282.2, - "customerRate": 177.54, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:18.505Z", - "updatedAt": "2021-05-30T16:03:42.520Z", + "createdAt": "2021-05-30T11:48:52.006Z", + "updatedAt": "2021-05-30T16:11:58.578Z", "payments": [] }, { - "id": "9c976d1a-f395-4889-ac9b-38846a083dcb", - "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", - "userHandle": "marathon_zhang", - "projectId": 17103, + "id": "279989ed-1e9e-45e9-bdb8-26c03d396344", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, "startDate": "2021-01-24", "endDate": "2021-01-30", - "daysWorked": 5, - "memberRate": 158.66, - "customerRate": 158.21, + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:18.500Z", - "updatedAt": "2021-05-30T16:04:11.932Z", + "createdAt": "2021-05-30T11:48:51.981Z", + "updatedAt": "2021-05-30T16:05:33.514Z", "payments": [] }, { - "id": "62bf7ac9-bea9-4f96-8a28-2a3a8dbbc48f", - "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", - "userHandle": "marathon_zhang", - "projectId": 17103, - "startDate": "2021-01-03", - "endDate": "2021-01-09", + "id": "31530732-4c7a-429c-9384-c219f16590fa", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 21.58, - "customerRate": 10.21, - "paymentStatus": "partially-completed", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:18.439Z", - "updatedAt": "2021-05-30T16:04:50.801Z", + "createdAt": "2021-05-30T11:48:51.988Z", + "updatedAt": "2021-05-30T16:05:27.123Z", "payments": [] }, { - "id": "05fb419d-927c-4264-b346-905ba7a55f49", - "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", - "userHandle": "marathon_zhang", - "projectId": 17103, + "id": "91065330-b979-4b3c-b084-0e14b6be6740", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, "startDate": "2021-01-31", "endDate": "2021-02-06", - "daysWorked": 2, - "memberRate": 294.55, - "customerRate": 40.52, - "paymentStatus": "partially-completed", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:18.492Z", - "updatedAt": "2021-05-30T16:12:15.133Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:51.984Z", + "updatedAt": "2021-05-30T16:04:19.946Z", "payments": [] }, { - "id": "c4b535c4-0c6f-4420-930e-0103aea68057", - "resourceBookingId": "0da35f26-f0cc-4f4d-b239-68c11b9a1fa3", - "userHandle": "marathon_zhang", - "projectId": 17103, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": 4, - "memberRate": 42.79, - "customerRate": 298.27, + "id": "06187a37-d29a-4bdb-bcb1-e0e7f57eec4a", + "resourceBookingId": "72db31b8-f05c-497c-9bc6-b9f6692569a0", + "userHandle": "centurionme", + "projectId": 16718, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:18.490Z", - "updatedAt": "2021-05-30T16:03:36.317Z", + "createdAt": "2021-05-30T11:48:51.978Z", + "updatedAt": "2021-05-30T16:06:00.996Z", "payments": [] } ] }, { - "id": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", - "projectId": 17091, - "userId": "de029f4b-f07b-4f8e-bc58-d928b8d8d289", - "jobId": "fb8b92f6-4ffb-4ba6-8c38-c2d4a151f76b", + "id": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "projectId": 16870, + "userId": "cc959274-bb53-4612-a4f4-af62496b026c", + "jobId": "fe8da845-5313-496f-b859-9824bd06a0db", "status": "placed", - "startDate": "2021-01-11", - "endDate": "2021-02-11", - "memberRate": 271.93, - "customerRate": 258.37, + "startDate": "2021-01-12", + "endDate": "2021-02-12", + "memberRate": 240.84, + "customerRate": 188.33, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-01T09:46:05.754Z", - "updatedAt": "2021-05-30T11:49:14.166Z", + "createdAt": "2021-05-01T09:30:37.276Z", + "updatedAt": "2021-05-30T11:48:54.737Z", "workPeriods": [ { - "id": "f234f6bb-a90f-4f2d-a205-24ac45f09246", - "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", - "userHandle": "kagematya", - "projectId": 17091, + "id": "c44339a8-5562-493d-9e59-02fded34dadd", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, "startDate": "2021-01-17", "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 273.55, - "customerRate": 245.82, - "paymentStatus": "cancelled", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:14.947Z", - "updatedAt": "2021-05-30T16:02:56.406Z", + "createdAt": "2021-05-30T11:48:55.482Z", + "updatedAt": "2021-05-30T16:03:38.050Z", "payments": [] }, { - "id": "2962c8d7-aeab-422e-aa9e-fd76a2c559d6", - "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", - "userHandle": "kagematya", - "projectId": 17091, - "startDate": "2021-02-07", - "endDate": "2021-02-13", - "daysWorked": 4, - "memberRate": 228.98, - "customerRate": 158.21, + "id": "6e8627d9-f3b9-4e56-ba48-0d4cd0572beb", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:15.005Z", - "updatedAt": "2021-05-30T16:12:13.268Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:55.537Z", + "updatedAt": "2021-05-30T16:04:39.704Z", "payments": [] }, { - "id": "729c31fb-dcd7-4b1e-bab8-b47f2db27f12", - "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", - "userHandle": "kagematya", - "projectId": 17091, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "36abc507-0e01-46e3-ab78-52c0e8f848b1", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", "daysWorked": 5, - "memberRate": 45.72, - "customerRate": 131.63, - "paymentStatus": "cancelled", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:14.943Z", - "updatedAt": "2021-05-30T16:04:37.868Z", + "createdAt": "2021-05-30T11:48:55.539Z", + "updatedAt": "2021-05-30T16:05:22.767Z", "payments": [] }, { - "id": "4dff33bc-ef83-425c-a07d-f49a12e2485f", - "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", - "userHandle": "kagematya", - "projectId": 17091, - "startDate": "2021-01-31", - "endDate": "2021-02-06", + "id": "20b18eeb-a78f-4ff2-8a3b-fbd1cfba567c", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, + "startDate": "2021-02-07", + "endDate": "2021-02-13", "daysWorked": 5, - "memberRate": 200.17, - "customerRate": 286.15, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:14.956Z", - "updatedAt": "2021-05-30T16:05:07.460Z", + "createdAt": "2021-05-30T11:48:55.526Z", + "updatedAt": "2021-05-30T16:05:40.643Z", "payments": [] }, { - "id": "2b2aaaba-2698-4b32-b6b9-e31e040ee023", - "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", - "userHandle": "kagematya", - "projectId": 17091, + "id": "4b8cc238-bb26-4fbf-ab74-a86c1d9a47ce", + "resourceBookingId": "6a4e3e22-5241-4353-94a2-f1ec0c3002e7", + "userHandle": "MikeKusold", + "projectId": 16870, "startDate": "2021-01-10", "endDate": "2021-01-16", - "daysWorked": 5, - "memberRate": 239.68, - "customerRate": 12.51, + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:14.958Z", - "updatedAt": "2021-05-30T16:05:31.643Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:55.532Z", + "updatedAt": "2021-05-30T16:12:00.505Z", "payments": [] } ] }, { - "id": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", - "projectId": 16718, - "userId": "085fc95d-0336-4572-a641-6c8334e7f0c9", - "jobId": "fb2f5f9b-5874-4dcd-af94-727fc0409760", - "status": "sourcing", - "startDate": "2021-01-01", - "endDate": "2021-02-01", - "memberRate": 114.33, - "customerRate": 100.25, + "id": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "projectId": 16805, + "userId": "3797d69c-0bf1-421e-b086-81e36ec1f929", + "jobId": "fc58382a-31d7-44b7-bfe5-2d671300f8d9", + "status": "placed", + "startDate": "2021-01-11", + "endDate": "2021-02-11", + "memberRate": 1.59, + "customerRate": 170.64, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-26T08:27:33.886Z", - "updatedAt": "2021-05-30T11:49:22.899Z", + "createdAt": "2021-05-28T05:15:29.662Z", + "updatedAt": "2021-05-30T11:48:37.381Z", "workPeriods": [ { - "id": "ddcfc959-d749-45dc-9e9f-f18a893f9e1a", - "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", - "userHandle": "george0095", - "projectId": 16718, - "startDate": "2020-12-27", - "endDate": "2021-01-02", - "daysWorked": 1, - "memberRate": 216.18, - "customerRate": 269.78, + "id": "33b3b539-5741-49af-a700-fa8e9bd4abba", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, + "startDate": "2021-01-24", + "endDate": "2021-01-30", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:23.644Z", - "updatedAt": "2021-05-30T16:12:17.859Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:38.136Z", + "updatedAt": "2021-05-30T16:05:25.424Z", "payments": [] }, { - "id": "a322ee7e-7f23-4f9f-b2d8-286b574efd7f", - "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", - "userHandle": "george0095", - "projectId": 16718, - "startDate": "2021-01-24", - "endDate": "2021-01-30", - "daysWorked": 5, - "memberRate": 115.09, - "customerRate": 107.07, + "id": "eca90916-ded1-49d5-8beb-582bba178dd9", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:23.681Z", - "updatedAt": "2021-05-30T16:04:07.452Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:38.215Z", + "updatedAt": "2021-05-30T16:11:50.205Z", "payments": [] }, { - "id": "77f9b42c-6e67-4363-8b43-aa0b70a904e1", - "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", - "userHandle": "george0095", - "projectId": 16718, + "id": "2f176676-60f7-4d27-bb79-d1183eb0b7e0", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, "startDate": "2021-01-17", "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 295.92, - "customerRate": 38.47, - "paymentStatus": "completed", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:23.734Z", - "updatedAt": "2021-05-30T16:04:29.823Z", - "payments": [] - }, - { - "id": "20fc029b-108a-4f12-aec9-ba36619d4ce7", - "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", - "userHandle": "george0095", - "projectId": 16718, - "startDate": "2021-01-31", - "endDate": "2021-02-06", - "daysWorked": 1, - "memberRate": 124.66, - "customerRate": 105, - "paymentStatus": "partially-completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:23.730Z", - "updatedAt": "2021-05-30T16:12:17.860Z", + "createdAt": "2021-05-30T11:48:38.152Z", + "updatedAt": "2021-05-30T16:05:29.869Z", "payments": [] }, { - "id": "662f11e5-c02e-460d-989e-1396ff4f00a6", - "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", - "userHandle": "george0095", - "projectId": 16718, + "id": "0ac738d8-03be-4ba4-a86b-2a1f65666cd5", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, "startDate": "2021-01-10", "endDate": "2021-01-16", - "daysWorked": 4, - "memberRate": 85.69, - "customerRate": 289.93, + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:23.678Z", - "updatedAt": "2021-05-30T16:04:45.827Z", + "createdAt": "2021-05-30T11:48:38.220Z", + "updatedAt": "2021-05-30T16:05:59.284Z", "payments": [] }, { - "id": "f8d56b84-b374-4975-81f8-7fab96463243", - "resourceBookingId": "04cb749b-6e23-4e5b-b5a9-f2b4d25a94a6", - "userHandle": "george0095", - "projectId": 16718, - "startDate": "2021-01-03", - "endDate": "2021-01-09", - "daysWorked": null, - "memberRate": 230.93, - "customerRate": 99.32, + "id": "b180bd57-30b3-4092-affc-c306401edd7d", + "resourceBookingId": "a331f572-8df0-4e00-8573-6aa09431e3d9", + "userHandle": "cp185035", + "projectId": 16805, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:23.666Z", - "updatedAt": "2021-05-30T16:02:49.121Z", + "createdAt": "2021-05-30T11:48:38.150Z", + "updatedAt": "2021-05-30T16:03:57.617Z", "payments": [] } ] }, { - "id": "1511406b-9d2b-43f0-99b6-2117d1012aaf", - "projectId": 16870, - "userId": "46550d28-0f34-4292-908f-02f1a34ac278", - "jobId": "fe539bef-9119-4a8c-b7b0-915e7e3a3ba3", + "id": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "projectId": 16739, + "userId": "3ed9015f-09d8-4173-bfcd-5dcc60c52060", + "jobId": "fc5ba131-566f-46fe-8501-79c593241896", "status": "placed", - "startDate": "2021-01-13", - "endDate": "2021-02-13", - "memberRate": 85.22, - "customerRate": 265.1, - "rateType": "weekly", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 61.33, + "customerRate": 114.05, + "rateType": "hourly", "billingAccountId": 80000071, - "createdBy": "00000000-0000-0000-0000-000000000000", + "createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-01T08:45:58.009Z", - "updatedAt": "2021-05-30T11:49:12.395Z", - "workPeriods": [ - { - "id": "dae10e27-1bec-4004-adb9-25a09a29f58d", - "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", - "userHandle": "prasanna992", - "projectId": 16870, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": 3, - "memberRate": 82.71, - "customerRate": 103.9, - "paymentStatus": "partially-completed", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:13.147Z", - "updatedAt": "2021-05-30T16:12:12.417Z", - "payments": [] - }, - { - "id": "60cfd6f3-4eed-4e2f-98be-f1377648d700", - "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", - "userHandle": "prasanna992", - "projectId": 16870, - "startDate": "2021-02-07", - "endDate": "2021-02-13", + "createdAt": "2021-05-17T13:41:56.896Z", + "updatedAt": "2021-05-30T11:48:29.760Z", + "workPeriods": [ + { + "id": "1dc38edd-3b56-4ed3-ae6c-ea2527076b32", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 30.12, - "customerRate": 229.65, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:13.155Z", - "updatedAt": "2021-05-30T16:04:52.604Z", + "createdAt": "2021-05-30T11:48:30.606Z", + "updatedAt": "2021-05-30T16:05:46.841Z", "payments": [] }, { - "id": "5356e3d0-fa3e-4a4a-a94b-3d58745c09f7", - "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", - "userHandle": "prasanna992", - "projectId": 16870, + "id": "212909c4-b1e9-4d12-b2ca-4175ccbb2d7f", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, "startDate": "2021-01-31", "endDate": "2021-02-06", - "daysWorked": 5, - "memberRate": 191.92, - "customerRate": 271.77, + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:13.213Z", - "updatedAt": "2021-05-30T16:05:01.471Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:30.530Z", + "updatedAt": "2021-05-30T16:11:45.643Z", "payments": [] }, { - "id": "394196b1-7fde-4b3e-a6f2-1d95cd93c27d", - "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", - "userHandle": "prasanna992", - "projectId": 16870, + "id": "d4dffbb9-2224-4429-9d7e-4bd9d33dba70", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, "startDate": "2021-01-24", "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 257.15, - "customerRate": 31.9, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:13.168Z", - "updatedAt": "2021-05-30T16:05:21.118Z", + "createdAt": "2021-05-30T11:48:30.547Z", + "updatedAt": "2021-05-30T16:03:22.384Z", "payments": [] }, { - "id": "a6fa8266-f335-4148-96a7-3f63dc66aec4", - "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", - "userHandle": "prasanna992", - "projectId": 16870, + "id": "b6b60c49-615a-4367-b644-af68485b4293", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:48:30.540Z", + "updatedAt": "2021-05-30T16:11:45.644Z", + "payments": [] + }, + { + "id": "7d495eed-d042-4a96-beed-dc2f2c1054c1", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, "startDate": "2021-01-17", "endDate": "2021-01-23", - "daysWorked": 4, - "memberRate": 159.75, - "customerRate": 168.78, + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:13.142Z", - "updatedAt": "2021-05-30T16:04:03.881Z", + "createdAt": "2021-05-30T11:48:30.527Z", + "updatedAt": "2021-05-30T16:04:25.202Z", + "payments": [] + }, + { + "id": "7468173d-6d92-4560-802e-6329ab656754", + "resourceBookingId": "c9f268af-a03f-476e-a58b-1a2bb52324e0", + "userHandle": "epicdoom", + "projectId": 16739, + "startDate": "2021-01-03", + "endDate": "2021-01-09", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:48:30.544Z", + "updatedAt": "2021-05-30T16:04:36.153Z", "payments": [] } ] }, { - "id": "0957b870-fc53-4343-8dbf-ebd3994b5734", - "projectId": 17103, - "userId": "9e4b1242-9b14-4159-bd0b-de7fa1803ca9", - "jobId": "feef8b66-989d-4ec7-bdb0-59ca05c95003", - "status": "cancelled", - "startDate": "2021-01-21", - "endDate": "2021-02-21", - "memberRate": 61.33, - "customerRate": 196.21, + "id": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "projectId": 17324, + "userId": "9807980a-a9e4-4f24-a48b-311fcdbf1f47", + "jobId": "fefd2618-9b66-4431-9874-1d02d7a37d90", + "status": "placed", + "startDate": "2021-01-01", + "endDate": "2021-02-01", + "memberRate": 287.14, + "customerRate": 146.2, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-01T10:25:50.725Z", - "updatedAt": "2021-05-30T11:49:19.410Z", + "createdAt": "2021-05-27T05:05:05.125Z", + "updatedAt": "2021-05-30T11:49:06.302Z", "workPeriods": [ { - "id": "217f124d-37db-49a8-9cac-187c5c8b2905", - "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", - "userHandle": "ApolloZhang", - "projectId": 17103, - "startDate": "2021-02-07", - "endDate": "2021-02-13", - "daysWorked": 4, - "memberRate": 157.6, - "customerRate": 211.33, + "id": "d711870a-4f78-431b-b5b5-ae5157999a0c", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:20.202Z", - "updatedAt": "2021-05-30T16:05:38.038Z", + "createdAt": "2021-05-30T11:49:07.137Z", + "updatedAt": "2021-05-30T16:03:20.574Z", "payments": [] }, { - "id": "b826c9b8-12f5-4567-bd62-df524bb690a2", - "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", - "userHandle": "ApolloZhang", - "projectId": 17103, - "startDate": "2021-01-31", - "endDate": "2021-02-06", + "id": "d0266458-f9d2-42db-a716-6f114b4a0be0", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-24", + "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 26.55, - "customerRate": 24.64, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:20.194Z", - "updatedAt": "2021-05-30T16:03:47.803Z", + "createdAt": "2021-05-30T11:49:07.095Z", + "updatedAt": "2021-05-30T16:03:25.900Z", "payments": [] }, { - "id": "3e6436c6-f6d4-4b33-8027-1269b167554f", - "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", - "userHandle": "ApolloZhang", - "projectId": 17103, - "startDate": "2021-01-24", - "endDate": "2021-01-30", + "id": "248de422-69c3-4c5b-8919-ba18113d0350", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-10", + "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 174.63, - "customerRate": 234.94, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:20.200Z", - "updatedAt": "2021-05-30T16:05:18.330Z", + "createdAt": "2021-05-30T11:49:07.106Z", + "updatedAt": "2021-05-30T16:05:36.306Z", "payments": [] }, { - "id": "56f49073-e651-479c-967c-8ba58e36b8e6", - "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", - "userHandle": "ApolloZhang", - "projectId": 17103, - "startDate": "2021-01-17", - "endDate": "2021-01-23", - "daysWorked": 2, - "memberRate": 166.58, - "customerRate": 107.72, + "id": "6aabe458-6e77-4fbd-9092-d811e7bbd21d", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:20.236Z", - "updatedAt": "2021-05-30T16:12:15.994Z", + "createdAt": "2021-05-30T11:49:07.144Z", + "updatedAt": "2021-05-30T16:12:08.558Z", "payments": [] }, { - "id": "f04d5bb8-abba-4447-92f0-005e823238f8", - "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", - "userHandle": "ApolloZhang", - "projectId": 17103, - "startDate": "2021-02-21", - "endDate": "2021-02-27", - "daysWorked": 0, - "memberRate": 143.95, - "customerRate": 249.53, + "id": "662931c7-09a4-43d9-a838-cb275296e818", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2020-12-27", + "endDate": "2021-01-02", + "daysWorked": 1, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:20.193Z", - "updatedAt": "2021-05-30T16:12:15.996Z", + "createdAt": "2021-05-30T11:49:07.100Z", + "updatedAt": "2021-05-30T16:12:08.560Z", "payments": [] }, { - "id": "fe874682-2ba6-4f42-929b-efc9e05adafd", - "resourceBookingId": "0957b870-fc53-4343-8dbf-ebd3994b5734", - "userHandle": "ApolloZhang", - "projectId": 17103, - "startDate": "2021-02-14", - "endDate": "2021-02-20", + "id": "11d7db8c-b4a9-47b0-b24a-e45d4dc5fae4", + "resourceBookingId": "1fd9cc33-d0ae-4be2-865b-95bc95c71700", + "userHandle": "bone2", + "projectId": 17324, + "startDate": "2021-01-03", + "endDate": "2021-01-09", "daysWorked": 5, - "memberRate": 186.09, - "customerRate": 217.18, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:20.197Z", - "updatedAt": "2021-05-30T15:58:40.816Z", + "createdAt": "2021-05-30T11:49:07.103Z", + "updatedAt": "2021-05-30T16:05:53.988Z", "payments": [] } ] }, { - "id": "0eae9b44-6764-46c4-ba13-4cec37bf8574", - "projectId": 17324, - "userId": "4709473d-f060-4102-87f8-4d51ff0b34c1", - "jobId": "fefd2618-9b66-4431-9874-1d02d7a37d90", + "id": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "projectId": 17091, + "userId": "de029f4b-f07b-4f8e-bc58-d928b8d8d289", + "jobId": "fb8b92f6-4ffb-4ba6-8c38-c2d4a151f76b", "status": "placed", "startDate": "2021-01-11", "endDate": "2021-02-11", "memberRate": 271.93, - "customerRate": 188.33, + "customerRate": 258.37, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-20T06:52:40.679Z", - "updatedAt": "2021-05-30T11:49:15.876Z", + "createdAt": "2021-05-01T09:46:05.754Z", + "updatedAt": "2021-05-30T11:49:14.166Z", "workPeriods": [ { - "id": "f28bd617-dce3-47c0-a9ab-6b2ff321d206", - "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", - "userHandle": "TCConnCopilot", - "projectId": 17324, + "id": "f234f6bb-a90f-4f2d-a205-24ac45f09246", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:14.947Z", + "updatedAt": "2021-05-30T16:02:56.406Z", + "payments": [] + }, + { + "id": "2962c8d7-aeab-422e-aa9e-fd76a2c559d6", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, "startDate": "2021-02-07", "endDate": "2021-02-13", "daysWorked": 4, - "memberRate": 185.99, - "customerRate": 213.97, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:16.700Z", - "updatedAt": "2021-05-30T16:12:14.202Z", + "createdAt": "2021-05-30T11:49:15.005Z", + "updatedAt": "2021-05-30T16:12:13.268Z", "payments": [] - }, - { - "id": "c447a850-2549-4c6a-ad3e-47cb6b26ac0b", - "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", - "userHandle": "TCConnCopilot", - "projectId": 17324, + }, + { + "id": "729c31fb-dcd7-4b1e-bab8-b47f2db27f12", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, "startDate": "2021-01-24", "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 22.66, - "customerRate": 215.7, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:16.706Z", - "updatedAt": "2021-05-30T16:03:37.121Z", + "createdAt": "2021-05-30T11:49:14.943Z", + "updatedAt": "2021-05-30T16:04:37.868Z", "payments": [] }, { - "id": "66732a8f-7bab-4e46-8eda-c58f28344114", - "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", - "userHandle": "TCConnCopilot", - "projectId": 17324, + "id": "4dff33bc-ef83-425c-a07d-f49a12e2485f", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, "startDate": "2021-01-31", "endDate": "2021-02-06", "daysWorked": 5, - "memberRate": 82.71, - "customerRate": 24.46, - "paymentStatus": "partially-completed", + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:16.759Z", - "updatedAt": "2021-05-30T16:04:44.983Z", + "createdAt": "2021-05-30T11:49:14.956Z", + "updatedAt": "2021-05-30T16:05:07.460Z", "payments": [] }, { - "id": "444fbe9a-616e-443a-a1b5-aadfe7c617ff", - "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", - "userHandle": "TCConnCopilot", - "projectId": 17324, + "id": "2b2aaaba-2698-4b32-b6b9-e31e040ee023", + "resourceBookingId": "0ffde888-a7d5-4ca7-8bd3-eea54f7c05f2", + "userHandle": "kagematya", + "projectId": 17091, "startDate": "2021-01-10", "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 230.09, - "customerRate": 248.77, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:16.687Z", - "updatedAt": "2021-05-30T16:05:13.852Z", - "payments": [] - }, - { - "id": "50865971-2fd2-4576-a799-8ab438e9dd75", - "resourceBookingId": "0eae9b44-6764-46c4-ba13-4cec37bf8574", - "userHandle": "TCConnCopilot", - "projectId": 17324, - "startDate": "2021-01-17", - "endDate": "2021-01-23", - "daysWorked": 4, - "memberRate": 10.25, - "customerRate": 159.65, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:16.701Z", - "updatedAt": "2021-05-30T16:05:05.541Z", + "createdAt": "2021-05-30T11:49:14.958Z", + "updatedAt": "2021-05-30T16:05:31.643Z", "payments": [] } ] }, { - "id": "1ad758ab-c19f-4247-954a-4581420aba8a", - "projectId": 17363, - "userId": "dbf68f12-69a4-4592-a0ab-cf68d9ed7ae4", - "jobId": "fd48d96e-b0f2-43b7-8a48-f4fa194d6bc8", + "id": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "projectId": 16870, + "userId": "46550d28-0f34-4292-908f-02f1a34ac278", + "jobId": "fe539bef-9119-4a8c-b7b0-915e7e3a3ba3", "status": "placed", - "startDate": "2021-01-11", - "endDate": "2021-02-11", - "memberRate": 114.33, - "customerRate": 217.99, + "startDate": "2021-01-13", + "endDate": "2021-02-13", + "memberRate": 85.22, + "customerRate": 265.1, "rateType": "weekly", "billingAccountId": 80000071, "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-27T11:25:35.292Z", - "updatedAt": "2021-05-30T11:49:08.019Z", + "createdAt": "2021-05-01T08:45:58.009Z", + "updatedAt": "2021-05-30T11:49:12.395Z", "workPeriods": [ { - "id": "c4c8588e-68a4-4b82-be91-a3d98661ffba", - "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", - "userHandle": "vishalgoel", - "projectId": 17363, - "startDate": "2021-01-17", - "endDate": "2021-01-23", - "daysWorked": 5, - "memberRate": 54.99, - "customerRate": 109.55, + "id": "dae10e27-1bec-4004-adb9-25a09a29f58d", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-01-10", + "endDate": "2021-01-16", + "daysWorked": 3, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:08.879Z", - "updatedAt": "2021-05-30T16:03:35.487Z", + "updatedBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-05-30T11:49:13.147Z", + "updatedAt": "2021-05-30T16:12:12.417Z", "payments": [] }, { - "id": "83cb4174-6ee3-4557-97c1-120c46054af6", - "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", - "userHandle": "vishalgoel", - "projectId": 17363, - "startDate": "2021-01-24", - "endDate": "2021-01-30", - "daysWorked": null, - "memberRate": 213.59, - "customerRate": 176.11, + "id": "60cfd6f3-4eed-4e2f-98be-f1377648d700", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-02-07", + "endDate": "2021-02-13", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:08.882Z", - "updatedAt": "2021-05-30T16:04:22.560Z", + "createdAt": "2021-05-30T11:49:13.155Z", + "updatedAt": "2021-05-30T16:04:52.604Z", "payments": [] }, { - "id": "8ff2339f-d90a-4ac2-9798-3158d0746d53", - "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", - "userHandle": "vishalgoel", - "projectId": 17363, - "startDate": "2021-02-07", - "endDate": "2021-02-13", - "daysWorked": 4, - "memberRate": 298.88, - "customerRate": 235.7, - "paymentStatus": "cancelled", + "id": "5356e3d0-fa3e-4a4a-a94b-3d58745c09f7", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-01-31", + "endDate": "2021-02-06", + "daysWorked": 5, + "daysPaid": 0, + "paymentTotal": 0, + "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:08.875Z", - "updatedAt": "2021-05-30T16:12:09.701Z", + "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", + "createdAt": "2021-05-30T11:49:13.213Z", + "updatedAt": "2021-05-30T16:05:01.471Z", "payments": [] }, { - "id": "5ecaa40c-1fb3-4df7-9870-6fc3c2bc1bca", - "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", - "userHandle": "vishalgoel", - "projectId": 17363, - "startDate": "2021-01-10", - "endDate": "2021-01-16", + "id": "394196b1-7fde-4b3e-a6f2-1d95cd93c27d", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-01-24", + "endDate": "2021-01-30", "daysWorked": 5, - "memberRate": 114.59, - "customerRate": 203.92, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:08.877Z", - "updatedAt": "2021-05-30T16:04:53.463Z", + "createdAt": "2021-05-30T11:49:13.168Z", + "updatedAt": "2021-05-30T16:05:21.118Z", "payments": [] }, { - "id": "38afaa09-32da-4d81-b2f5-0c5e31af617f", - "resourceBookingId": "1ad758ab-c19f-4247-954a-4581420aba8a", - "userHandle": "vishalgoel", - "projectId": 17363, - "startDate": "2021-01-31", - "endDate": "2021-02-06", - "daysWorked": 5, - "memberRate": 176.02, - "customerRate": 47.87, + "id": "a6fa8266-f335-4148-96a7-3f63dc66aec4", + "resourceBookingId": "1511406b-9d2b-43f0-99b6-2117d1012aaf", + "userHandle": "prasanna992", + "projectId": 16870, + "startDate": "2021-01-17", + "endDate": "2021-01-23", + "daysWorked": 4, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:08.867Z", - "updatedAt": "2021-05-30T16:05:21.960Z", + "createdAt": "2021-05-30T11:49:13.142Z", + "updatedAt": "2021-05-30T16:04:03.881Z", "payments": [] } ] @@ -6858,8 +7325,8 @@ "startDate": "2021-01-31", "endDate": "2021-02-06", "daysWorked": 1, - "memberRate": 204.06, - "customerRate": 96.56, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", @@ -6875,8 +7342,8 @@ "startDate": "2020-12-27", "endDate": "2021-01-02", "daysWorked": 1, - "memberRate": 46.62, - "customerRate": 280.07, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", @@ -6892,8 +7359,8 @@ "startDate": "2021-01-17", "endDate": "2021-01-23", "daysWorked": 5, - "memberRate": 108.35, - "customerRate": 298.27, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -6909,8 +7376,8 @@ "startDate": "2021-01-10", "endDate": "2021-01-16", "daysWorked": 5, - "memberRate": 18.57, - "customerRate": 272.37, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -6926,8 +7393,8 @@ "startDate": "2021-01-03", "endDate": "2021-01-09", "daysWorked": 5, - "memberRate": 266.82, - "customerRate": 268.61, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -6943,8 +7410,8 @@ "startDate": "2021-01-24", "endDate": "2021-01-30", "daysWorked": 4, - "memberRate": 296.93, - "customerRate": 255.03, + "daysPaid": 0, + "paymentTotal": 0, "paymentStatus": "pending", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", @@ -6954,110 +7421,6 @@ } ] }, - { - "id": "07f73049-e51a-4394-b61f-b75418afa908", - "projectId": 16739, - "userId": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "jobId": "fc5ba131-566f-46fe-8501-79c593241896", - "status": "placed", - "startDate": "2021-01-11", - "endDate": "2021-02-11", - "memberRate": 66, - "customerRate": 114.05, - "rateType": "weekly", - "billingAccountId": 80000071, - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-01-12T10:59:20.006Z", - "updatedAt": "2021-05-30T11:49:21.188Z", - "workPeriods": [ - { - "id": "d99a524f-c8a4-4d46-a42c-dbcddd65b6db", - "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", - "userHandle": "nkumartest", - "projectId": 16739, - "startDate": "2021-01-31", - "endDate": "2021-02-06", - "daysWorked": 5, - "memberRate": 214.14, - "customerRate": 212.49, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:22.017Z", - "updatedAt": "2021-05-30T16:03:19.706Z", - "payments": [] - }, - { - "id": "5c4bb82b-e617-4c91-861f-5e0825d43c53", - "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", - "userHandle": "nkumartest", - "projectId": 16739, - "startDate": "2021-01-10", - "endDate": "2021-01-16", - "daysWorked": 5, - "memberRate": 70.84, - "customerRate": 136.39, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:21.977Z", - "updatedAt": "2021-05-30T16:04:55.294Z", - "payments": [] - }, - { - "id": "7ff4804e-2a65-4f8b-af5b-24b58c066fd4", - "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", - "userHandle": "nkumartest", - "projectId": 16739, - "startDate": "2021-02-07", - "endDate": "2021-02-13", - "daysWorked": 4, - "memberRate": 25.99, - "customerRate": 122.72, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "00000000-0000-0000-0000-000000000000", - "createdAt": "2021-05-30T11:49:22.068Z", - "updatedAt": "2021-05-30T16:12:16.993Z", - "payments": [] - }, - { - "id": "da043aba-161e-4894-a3d5-d63678ac89b0", - "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", - "userHandle": "nkumartest", - "projectId": 16739, - "startDate": "2021-01-24", - "endDate": "2021-01-30", - "daysWorked": null, - "memberRate": 71.99, - "customerRate": 155.48, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:21.974Z", - "updatedAt": "2021-05-30T16:03:18.831Z", - "payments": [] - }, - { - "id": "410e034d-ee48-4a18-aa65-679ef7efcb80", - "resourceBookingId": "07f73049-e51a-4394-b61f-b75418afa908", - "userHandle": "nkumartest", - "projectId": 16739, - "startDate": "2021-01-17", - "endDate": "2021-01-23", - "daysWorked": 4, - "memberRate": 146.28, - "customerRate": 21.33, - "paymentStatus": "pending", - "createdBy": "00000000-0000-0000-0000-000000000000", - "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", - "createdAt": "2021-05-30T11:49:21.961Z", - "updatedAt": "2021-05-30T16:05:16.466Z", - "payments": [] - } - ] - }, { "id": "d3cd14c8-9ae8-446a-b554-69240c93a20e", "projectId": 17091, diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 094ce95f..7f1ac21e 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "0f0620d2-acea-4e72-8d4c-6bb285d8a16a", + "_postman_id": "3a5bab78-49d6-4dca-9aea-e8688564ac98", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -10886,7 +10886,7 @@ " _.each(['id','projectId','status','startDate','endDate','billingAccountId','userId','jobId','rateType','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt','workPeriods'], field => {\r", " pm.expect(response).to.have.property(field)\r", " })\r", - " _.each(['id','projectId','paymentStatus','startDate','endDate','userHandle','resourceBookingId','daysWorked','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt'], field => {\r", + " _.each(['id','projectId','paymentStatus','startDate','endDate','userHandle','resourceBookingId','daysWorked','daysPaid','paymentTotal','createdBy','updatedBy','createdAt','updatedAt'], field => {\r", " pm.expect(response.workPeriods[0]).to.have.property(field)\r", " })\r", "});" @@ -10936,7 +10936,7 @@ " _.each(['id','projectId','status','startDate','endDate','billingAccountId','userId','jobId','rateType','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt','workPeriods'], field => {\r", " pm.expect(response).to.have.property(field)\r", " })\r", - " _.each(['id','projectId','paymentStatus','startDate','endDate','userHandle','resourceBookingId','daysWorked','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt'], field => {\r", + " _.each(['id','projectId','paymentStatus','startDate','endDate','userHandle','resourceBookingId','daysWorked','daysPaid','paymentTotal','createdBy','updatedBy','createdAt','updatedAt'], field => {\r", " pm.expect(response.workPeriods[0]).to.have.property(field)\r", " })\r", "});" @@ -11091,7 +11091,7 @@ "pm.test('Status code is 403', function () {\r", " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You don't have access to view memberRate\")\r", + " pm.expect(response.message).to.eq(\"You don't have access to view memberRate and paymentTotal\")\r", "});" ], "type": "text/javascript" @@ -11136,7 +11136,7 @@ "pm.test('Status code is 403', function () {\r", " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You don't have access to view memberRate\")\r", + " pm.expect(response.message).to.eq(\"You don't have access to view memberRate and paymentTotal\")\r", "});" ], "type": "text/javascript" @@ -11705,7 +11705,7 @@ " _.each(['id','projectId','status','startDate','endDate','billingAccountId','userId','jobId','rateType','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt','workPeriods'], field => {\r", " pm.expect(response[0]).to.have.property(field)\r", " })\r", - " _.each(['id','projectId','paymentStatus','startDate','endDate','userHandle','resourceBookingId','daysWorked','memberRate','customerRate','createdBy','updatedBy','createdAt','updatedAt'], field => {\r", + " _.each(['id','projectId','paymentStatus','startDate','endDate','userHandle','resourceBookingId','daysWorked','daysPaid','paymentTotal','createdBy','updatedBy','createdAt','updatedAt'], field => {\r", " pm.expect(response[0].workPeriods[0]).to.have.property(field)\r", " })\r", "});" @@ -12154,7 +12154,7 @@ "pm.test('Status code is 403', function () {\r", " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You don't have access to view memberRate\")\r", + " pm.expect(response.message).to.eq(\"You don't have access to view memberRate and paymentTotal\")\r", "});" ], "type": "text/javascript" @@ -12726,7 +12726,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"status\": \"placed\",\r\n \"startDate\": \"2020-09-30\",\r\n \"endDate\": \"2020-11-28\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"billingAccountId\": 80000071\r\n}", + "raw": "{\r\n \"status\": \"placed\",\r\n \"startDate\": \"2020-09-30\",\r\n \"endDate\": \"2020-11-28\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"billingAccountId\": 80000071\r\n}", "options": { "raw": { "language": "json" @@ -12772,7 +12772,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"status\": \"placed\",\r\n \"startDate\": \"2020-12-30\",\r\n \"endDate\": \"2021-02-10\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"billingAccountId\": 80000071\r\n}", + "raw": "{\r\n \"status\": \"placed\",\r\n \"startDate\": \"2020-12-30\",\r\n \"endDate\": \"2021-02-10\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"billingAccountId\": 80000071\r\n}", "options": { "raw": { "language": "json" @@ -13076,7 +13076,7 @@ "response": [] }, { - "name": "patch work period set status to completed", + "name": "post process payment for work period", "event": [ { "listen": "test", @@ -13091,7 +13091,7 @@ } ], "request": { - "method": "PATCH", + "method": "POST", "header": [ { "key": "Authorization", @@ -13101,7 +13101,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"paymentStatus\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodIdForPaid}}\"\r\n}", "options": { "raw": { "language": "json" @@ -13109,13 +13109,12 @@ } }, "url": { - "raw": "{{URL}}/work-periods/{{workPeriodIdForPaid}}", + "raw": "{{URL}}/work-period-payments", "host": [ "{{URL}}" ], "path": [ - "work-periods", - "{{workPeriodIdForPaid}}" + "work-period-payments" ] } }, @@ -13582,7 +13581,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27\",\r\n \"endDate\": \"2020-09-27\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"billingAccountId\": 80000071\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2021-06-01\",\r\n \"endDate\": \"2021-08-01\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"billingAccountId\": 80000071\r\n}", "options": { "raw": { "language": "json" @@ -13604,18 +13603,16 @@ ] }, { - "name": "create work period with booking manager", + "name": "create work period", "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(\"workPeriodId\", response.id);\r", - " }\r", + "pm.test('Status code is 405', function () {\r", + " pm.response.to.have.status(405);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"The requested HTTP method is not supported.\")\r", "});" ], "type": "text/javascript" @@ -13653,7 +13650,7 @@ "response": [] }, { - "name": "create work period with m2m create", + "name": "search work periods", "event": [ { "listen": "test", @@ -13661,10 +13658,12 @@ "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(\"workPeriodIdCreatedByM2M\", response.id);\r", - " }\r", + " const response = pm.response.json()\r", + " pm.environment.set(\"workPeriodId-1\", response[0].id);\r", + " pm.environment.set(\"workPeriodId-2\", response[1].id);\r", + " pm.environment.set(\"workPeriodId-3\", response[2].id);\r", + " pm.environment.set(\"workPeriodId-4\", response[3].id);\r", + " pm.environment.set(\"workPeriodId-5\", response[4].id);\r", "});" ], "type": "text/javascript" @@ -13672,46 +13671,88 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_m2m_create_work_period}}" + "value": "Bearer {{token_bookingManager}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-14\",\r\n \"endDate\": \"2021-03-20\",\r\n \"daysWorked\": 3,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"cancelled\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{URL}}/work-periods", + "raw": "{{URL}}/work-periods?sortBy=startDate&sortOrder=asc&resourceBookingId={{resourceBookingId}}&perPage=5", "host": [ "{{URL}}" ], "path": [ "work-periods" + ], + "query": [ + { + "key": "page", + "value": "1", + "disabled": true + }, + { + "key": "sortBy", + "value": "startDate" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "resourceBookingId", + "value": "{{resourceBookingId}}" + }, + { + "key": "resourceBookingIds", + "value": "{{resourceBookingId}},{{resourceBookingId}}", + "disabled": true + }, + { + "key": "paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-03-14", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-03-20", + "disabled": true + }, + { + "key": "userHandle", + "value": "pshah_manager", + "disabled": true + }, + { + "key": "projectId", + "value": "16843", + "disabled": true + }, + { + "key": "perPage", + "value": "5" + } ] } }, "response": [] }, { - "name": "create work period with connect user", + "name": "get work period with booking manager", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -13719,46 +13760,36 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_connectUser}}" + "value": "Bearer {{token_bookingManager}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-21\",\r\n \"endDate\": \"2021-03-27\",\r\n \"daysWorked\": 3,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"cancelled\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{URL}}/work-periods", + "raw": "{{URL}}/work-periods/{{workPeriodId-1}}", "host": [ "{{URL}}" ], "path": [ - "work-periods" + "work-periods", + "{{workPeriodId-1}}" ] } }, "response": [] }, { - "name": "create work period with member", + "name": "get work period with m2m read 1", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -13766,46 +13797,36 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_member}}" + "value": "Bearer {{token_m2m_read_work_period}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-21\",\r\n \"endDate\": \"2021-03-27\",\r\n \"daysWorked\": 3,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"cancelled\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{URL}}/work-periods", + "raw": "{{URL}}/work-periods/{{workPeriodId-2}}", "host": [ "{{URL}}" ], "path": [ - "work-periods" + "work-periods", + "{{workPeriodId-2}}" ] } }, "response": [] }, { - "name": "create work period with user id not exist", + "name": "get work period with m2m read 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(\"Bad Request\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -13813,46 +13834,36 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_userId_not_exist}}" + "value": "Bearer {{token_m2m_read_work_period_and_payment}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-21\",\r\n \"endDate\": \"2021-03-27\",\r\n \"daysWorked\": 3,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"cancelled\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{URL}}/work-periods", + "raw": "{{URL}}/work-periods/{{workPeriodId-3}}", "host": [ "{{URL}}" ], "path": [ - "work-periods" + "work-periods", + "{{workPeriodId-3}}" ] } }, "response": [] }, { - "name": "create work period with invalid token", + "name": "get work period with booking manager from db", "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", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -13860,46 +13871,42 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer invalid_token" + "value": "Bearer {{token_bookingManager}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-21\",\r\n \"endDate\": \"2021-03-27\",\r\n \"daysWorked\": 3,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"cancelled\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{URL}}/work-periods", + "raw": "{{URL}}/work-periods/{{workPeriodId-4}}?fromDb=true", "host": [ "{{URL}}" ], "path": [ - "work-periods" + "work-periods", + "{{workPeriodId-4}}" + ], + "query": [ + { + "key": "fromDb", + "value": "true" + } ] } }, "response": [] }, { - "name": "create work period with missing parameter 1", + "name": "get work period with connect user", "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(\"\\\"workPeriod.resourceBookingId\\\" is required\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -13907,46 +13914,36 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_connectUser}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{URL}}/work-periods", + "raw": "{{URL}}/work-periods/{{workPeriodId-5}}", "host": [ "{{URL}}" ], "path": [ - "work-periods" + "work-periods", + "{{workPeriodId-5}}" ] } }, "response": [] }, { - "name": "create work period with missing parameter 2", + "name": "get work period with member", "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(\"\\\"workPeriod.endDate\\\" is required\")\r", + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", "});" ], "type": "text/javascript" @@ -13954,46 +13951,36 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_member}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{URL}}/work-periods", + "raw": "{{URL}}/work-periods/{{workPeriodId-1}}", "host": [ "{{URL}}" ], "path": [ - "work-periods" + "work-periods", + "{{workPeriodId-1}}" ] } }, "response": [] }, { - "name": "create work period with missing parameter 3", + "name": "search work periods with booking manager", "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(\"\\\"workPeriod.paymentStatus\\\" is required\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -14001,7 +13988,7 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", @@ -14009,15 +13996,6 @@ "value": "Bearer {{token_bookingManager}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "{{URL}}/work-periods", "host": [ @@ -14025,69 +14003,77 @@ ], "path": [ "work-periods" - ] - } - }, - "response": [] - }, - { - "name": "create work period with invalid parameter 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(\"\\\"workPeriod.resourceBookingId\\\" must be a valid GUID\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"aaa-aaa\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods", - "host": [ - "{{URL}}" ], - "path": [ - "work-periods" + "query": [ + { + "key": "page", + "value": "1", + "disabled": true + }, + { + "key": "perPage", + "value": "35", + "disabled": true + }, + { + "key": "sortBy", + "value": "id", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "resourceBookingId", + "value": "{{resourceBookingId}}", + "disabled": true + }, + { + "key": "resourceBookingIds", + "value": "{{resourceBookingId}},{{resourceBookingIdCreatedByM2M}}", + "disabled": true + }, + { + "key": "paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-03-14", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-03-20", + "disabled": true + }, + { + "key": "userHandle", + "value": "pshah_manager", + "disabled": true + }, + { + "key": "projectId", + "value": "16843", + "disabled": true + } ] } }, "response": [] }, { - "name": "create work period with invalid parameter 2", + "name": "search work periods with m2m all 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(\"\\\"workPeriod.startDate\\\" must be in YYYY-MM-DD format\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -14095,23 +14081,14 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_m2m_all_work_period}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"07-03-2021\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "{{URL}}/work-periods", "host": [ @@ -14119,22 +14096,77 @@ ], "path": [ "work-periods" + ], + "query": [ + { + "key": "page", + "value": "1", + "disabled": true + }, + { + "key": "perPage", + "value": "5", + "disabled": true + }, + { + "key": "sortBy", + "value": "id", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "resourceBookingId", + "value": "{{resourceBookingId}}", + "disabled": true + }, + { + "key": "resourceBookingIds", + "value": "{{resourceBookingId}},{{resourceBookingIdCreatedByM2M}}", + "disabled": true + }, + { + "key": "paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-03-14", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-03-20", + "disabled": true + }, + { + "key": "userHandle", + "value": "pshah_manager", + "disabled": true + }, + { + "key": "projectId", + "value": "16843", + "disabled": true + } ] } }, "response": [] }, { - "name": "create work period with invalid parameter 3", + "name": "search work periods with m2m all 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(\"startDate should be always Sunday\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -14142,23 +14174,14 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_m2m_all_work_period_and_payment}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-06\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "{{URL}}/work-periods", "host": [ @@ -14166,22 +14189,77 @@ ], "path": [ "work-periods" + ], + "query": [ + { + "key": "page", + "value": "1", + "disabled": true + }, + { + "key": "perPage", + "value": "5", + "disabled": true + }, + { + "key": "sortBy", + "value": "id", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "resourceBookingId", + "value": "{{resourceBookingId}}", + "disabled": true + }, + { + "key": "resourceBookingIds", + "value": "{{resourceBookingId}},{{resourceBookingIdCreatedByM2M}}", + "disabled": true + }, + { + "key": "paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-03-14", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-03-20", + "disabled": true + }, + { + "key": "userHandle", + "value": "pshah_manager", + "disabled": true + }, + { + "key": "projectId", + "value": "16843", + "disabled": true + } ] } }, "response": [] }, { - "name": "create work period with invalid parameter 4", + "name": "search work periods with connect user", "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(\"endDate should be always the next Saturday\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -14189,23 +14267,14 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_connectUser}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-14\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "{{URL}}/work-periods", "host": [ @@ -14213,46 +14282,79 @@ ], "path": [ "work-periods" - ] - } - }, - "response": [] - }, - { - "name": "create work period with invalid parameter 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(\"\\\"workPeriod.daysWorked\\\" must be a number\")\r", - "});" - ], - "type": "text/javascript" - } + ], + "query": [ + { + "key": "page", + "value": "1", + "disabled": true + }, + { + "key": "perPage", + "value": "5", + "disabled": true + }, + { + "key": "sortBy", + "value": "id", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "resourceBookingId", + "value": "{{resourceBookingId}}", + "disabled": true + }, + { + "key": "resourceBookingIds", + "value": "{{resourceBookingId}},{{resourceBookingIdCreatedByM2M}}", + "disabled": true + }, + { + "key": "paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-03-14", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-03-20", + "disabled": true + }, + { + "key": "userHandle", + "value": "pshah_manager", + "disabled": true + }, + { + "key": "projectId", + "value": "16843", + "disabled": true + } + ] } - ], + }, + "response": [] + }, + { + "name": "search work periods with member", "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_member}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": \"aa\",\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "{{URL}}/work-periods", "host": [ @@ -14260,22 +14362,77 @@ ], "path": [ "work-periods" + ], + "query": [ + { + "key": "page", + "value": "1", + "disabled": true + }, + { + "key": "perPage", + "value": "5", + "disabled": true + }, + { + "key": "sortBy", + "value": "id", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "resourceBookingId", + "value": "{{resourceBookingId}}", + "disabled": true + }, + { + "key": "resourceBookingIds", + "value": "{{resourceBookingId}},{{resourceBookingIdCreatedByM2M}}", + "disabled": true + }, + { + "key": "paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-03-14", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-03-20", + "disabled": true + }, + { + "key": "userHandle", + "value": "pshah_manager", + "disabled": true + }, + { + "key": "projectId", + "value": "16843", + "disabled": true + } ] } }, "response": [] }, { - "name": "create work period with invalid parameter 6", + "name": "search work periods with invalid token", "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(`Key (resource_booking_id, start_date, end_date)=(${pm.environment.get('resourceBookingId')}, 2021-03-07, 2021-03-13) already exists.`)\r", + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", "});" ], "type": "text/javascript" @@ -14283,23 +14440,14 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer invalid_token" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "{{URL}}/work-periods", "host": [ @@ -14307,22 +14455,77 @@ ], "path": [ "work-periods" + ], + "query": [ + { + "key": "page", + "value": "1", + "disabled": true + }, + { + "key": "perPage", + "value": "5", + "disabled": true + }, + { + "key": "sortBy", + "value": "id", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "resourceBookingId", + "value": "{{resourceBookingId}}", + "disabled": true + }, + { + "key": "resourceBookingIds", + "value": "{{resourceBookingId}},{{resourceBookingIdCreatedByM2M}}", + "disabled": true + }, + { + "key": "paymentStatus", + "value": "pending", + "disabled": true + }, + { + "key": "startDate", + "value": "2021-03-14", + "disabled": true + }, + { + "key": "endDate", + "value": "2021-03-20", + "disabled": true + }, + { + "key": "userHandle", + "value": "pshah_manager", + "disabled": true + }, + { + "key": "projectId", + "value": "111", + "disabled": true + } ] } }, "response": [] }, { - "name": "create work period with invalid parameter 7", + "name": "put work period", "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(\"\\\"workPeriod.paymentStatus\\\" must be one of [pending, partially-completed, completed, cancelled]\")\r", + "pm.test('Status code is 404', function () {\r", + " pm.response.to.have.status(404);\r", "});" ], "type": "text/javascript" @@ -14330,7 +14533,7 @@ } ], "request": { - "method": "POST", + "method": "PUT", "header": [ { "key": "Authorization", @@ -14340,7 +14543,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"paid\"\r\n}", + "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", "options": { "raw": { "language": "json" @@ -14348,19 +14551,20 @@ } }, "url": { - "raw": "{{URL}}/work-periods", + "raw": "{{URL}}/work-periods/{{workPeriodId-1}}", "host": [ "{{URL}}" ], "path": [ - "work-periods" + "work-periods", + "{{workPeriodId-1}}" ] } }, "response": [] }, { - "name": "get work period with booking manager", + "name": "patch work period with booking manager", "event": [ { "listen": "test", @@ -14375,7 +14579,7 @@ } ], "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", @@ -14383,21 +14587,30 @@ "value": "Bearer {{token_bookingManager}}" } ], + "body": { + "mode": "raw", + "raw": "{\r\n \"daysWorked\": 3\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", + "raw": "{{URL}}/work-periods/{{workPeriodId-1}}", "host": [ "{{URL}}" ], "path": [ "work-periods", - "{{workPeriodId}}" + "{{workPeriodId-1}}" ] } }, "response": [] }, { - "name": "get work period with m2m read 1", + "name": "patch work period with m2m update", "event": [ { "listen": "test", @@ -14412,73 +14625,95 @@ } ], "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_m2m_read_work_period}}" + "value": "Bearer {{token_m2m_update_work_period}}" } ], + "body": { + "mode": "raw", + "raw": "{\r\n \"daysWorked\": 4\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "{{URL}}/work-periods/{{workPeriodIdCreatedByM2M}}", + "raw": "{{URL}}/work-periods/{{workPeriodId-2}}", "host": [ "{{URL}}" ], "path": [ "work-periods", - "{{workPeriodIdCreatedByM2M}}" + "{{workPeriodId-2}}" ] } }, "response": [] }, { - "name": "get work period with m2m read 2", + "name": "patch work period with connect user", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 200', function () {\r", - " pm.response.to.have.status(200);\r", - "});" + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" ], "type": "text/javascript" } } ], "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_m2m_read_work_period_and_payment}}" + "value": "Bearer {{token_connectUser}}" } ], + "body": { + "mode": "raw", + "raw": "{\r\n \"daysWorked\": 3\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "{{URL}}/work-periods/{{workPeriodIdCreatedByM2M}}", + "raw": "{{URL}}/work-periods/{{workPeriodId-3}}", "host": [ "{{URL}}" ], "path": [ "work-periods", - "{{workPeriodIdCreatedByM2M}}" + "{{workPeriodId-3}}" ] } }, "response": [] }, { - "name": "get work period with booking manager from db", + "name": "patch work period with member", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 200', function () {\r", - " pm.response.to.have.status(200);\r", + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", "});" ], "type": "text/javascript" @@ -14486,42 +14721,47 @@ } ], "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_member}}" } ], + "body": { + "mode": "raw", + "raw": "{\r\n \"daysWorked\": 3\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}?fromDb=true", + "raw": "{{URL}}/work-periods/{{workPeriodId-4}}", "host": [ "{{URL}}" ], "path": [ "work-periods", - "{{workPeriodId}}" - ], - "query": [ - { - "key": "fromDb", - "value": "true" - } + "{{workPeriodId-4}}" ] } }, "response": [] }, { - "name": "get work period with connect user", + "name": "patch work period with user id not exist", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 200', function () {\r", - " pm.response.to.have.status(200);\r", + "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(\"Bad Request\")\r", "});" ], "type": "text/javascript" @@ -14529,29 +14769,38 @@ } ], "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_connectUser}}" + "value": "Bearer {{token_userId_not_exist}}" } ], + "body": { + "mode": "raw", + "raw": "{\r\n \"daysWorked\": 3\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", + "raw": "{{URL}}/work-periods/{{workPeriodId-5}}", "host": [ "{{URL}}" ], "path": [ "work-periods", - "{{workPeriodId}}" + "{{workPeriodId-5}}" ] } }, "response": [] }, { - "name": "get work period with member", + "name": "patch work period with invalid token", "event": [ { "listen": "test", @@ -14559,6 +14808,8 @@ "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" @@ -14566,2596 +14817,47 @@ } ], "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_member}}" + "value": "Bearer invalid_token" } ], - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "search work periods with booking manager", - "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", - "type": "text", - "value": "Bearer {{token_bookingManager}}" + "body": { + "mode": "raw", + "raw": "{\r\n \"daysWorked\": 3\r\n}", + "options": { + "raw": { + "language": "json" + } } - ], + }, "url": { - "raw": "{{URL}}/work-periods", + "raw": "{{URL}}/work-periods/{{workPeriodId-1}}", "host": [ "{{URL}}" ], "path": [ - "work-periods" - ], - "query": [ - { - "key": "page", - "value": "1", - "disabled": true - }, - { - "key": "perPage", - "value": "35", - "disabled": true - }, - { - "key": "sortBy", - "value": "id", - "disabled": true - }, - { - "key": "sortOrder", - "value": "desc", - "disabled": true - }, - { - "key": "resourceBookingId", - "value": "{{resourceBookingId}}", - "disabled": true - }, - { - "key": "resourceBookingIds", - "value": "{{resourceBookingId}},{{resourceBookingIdCreatedByM2M}}", - "disabled": true - }, - { - "key": "paymentStatus", - "value": "pending", - "disabled": true - }, - { - "key": "startDate", - "value": "2021-03-14", - "disabled": true - }, - { - "key": "endDate", - "value": "2021-03-20", - "disabled": true - }, - { - "key": "userHandle", - "value": "pshah_manager", - "disabled": true - }, - { - "key": "projectId", - "value": "16843", - "disabled": true - } + "work-periods", + "{{workPeriodId-1}}" ] } }, "response": [] }, { - "name": "search work periods with m2m all 1", + "name": "patch work period with invalid parameter 1", "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", - "type": "text", - "value": "Bearer {{token_m2m_all_work_period}}" - } - ], - "url": { - "raw": "{{URL}}/work-periods", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods" - ], - "query": [ - { - "key": "page", - "value": "1", - "disabled": true - }, - { - "key": "perPage", - "value": "5", - "disabled": true - }, - { - "key": "sortBy", - "value": "id", - "disabled": true - }, - { - "key": "sortOrder", - "value": "desc", - "disabled": true - }, - { - "key": "resourceBookingId", - "value": "{{resourceBookingId}}", - "disabled": true - }, - { - "key": "resourceBookingIds", - "value": "{{resourceBookingId}},{{resourceBookingIdCreatedByM2M}}", - "disabled": true - }, - { - "key": "paymentStatus", - "value": "pending", - "disabled": true - }, - { - "key": "startDate", - "value": "2021-03-14", - "disabled": true - }, - { - "key": "endDate", - "value": "2021-03-20", - "disabled": true - }, - { - "key": "userHandle", - "value": "pshah_manager", - "disabled": true - }, - { - "key": "projectId", - "value": "16843", - "disabled": true - } - ] - } - }, - "response": [] - }, - { - "name": "search work periods with m2m all 2", - "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", - "type": "text", - "value": "Bearer {{token_m2m_all_work_period_and_payment}}" - } - ], - "url": { - "raw": "{{URL}}/work-periods", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods" - ], - "query": [ - { - "key": "page", - "value": "1", - "disabled": true - }, - { - "key": "perPage", - "value": "5", - "disabled": true - }, - { - "key": "sortBy", - "value": "id", - "disabled": true - }, - { - "key": "sortOrder", - "value": "desc", - "disabled": true - }, - { - "key": "resourceBookingId", - "value": "{{resourceBookingId}}", - "disabled": true - }, - { - "key": "resourceBookingIds", - "value": "{{resourceBookingId}},{{resourceBookingIdCreatedByM2M}}", - "disabled": true - }, - { - "key": "paymentStatus", - "value": "pending", - "disabled": true - }, - { - "key": "startDate", - "value": "2021-03-14", - "disabled": true - }, - { - "key": "endDate", - "value": "2021-03-20", - "disabled": true - }, - { - "key": "userHandle", - "value": "pshah_manager", - "disabled": true - }, - { - "key": "projectId", - "value": "16843", - "disabled": true - } - ] - } - }, - "response": [] - }, - { - "name": "search work periods with connect user", - "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", - "type": "text", - "value": "Bearer {{token_connectUser}}" - } - ], - "url": { - "raw": "{{URL}}/work-periods", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods" - ], - "query": [ - { - "key": "page", - "value": "1", - "disabled": true - }, - { - "key": "perPage", - "value": "5", - "disabled": true - }, - { - "key": "sortBy", - "value": "id", - "disabled": true - }, - { - "key": "sortOrder", - "value": "desc", - "disabled": true - }, - { - "key": "resourceBookingId", - "value": "{{resourceBookingId}}", - "disabled": true - }, - { - "key": "resourceBookingIds", - "value": "{{resourceBookingId}},{{resourceBookingIdCreatedByM2M}}", - "disabled": true - }, - { - "key": "paymentStatus", - "value": "pending", - "disabled": true - }, - { - "key": "startDate", - "value": "2021-03-14", - "disabled": true - }, - { - "key": "endDate", - "value": "2021-03-20", - "disabled": true - }, - { - "key": "userHandle", - "value": "pshah_manager", - "disabled": true - }, - { - "key": "projectId", - "value": "16843", - "disabled": true - } - ] - } - }, - "response": [] - }, - { - "name": "search work periods with member", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_member}}" - } - ], - "url": { - "raw": "{{URL}}/work-periods", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods" - ], - "query": [ - { - "key": "page", - "value": "1", - "disabled": true - }, - { - "key": "perPage", - "value": "5", - "disabled": true - }, - { - "key": "sortBy", - "value": "id", - "disabled": true - }, - { - "key": "sortOrder", - "value": "desc", - "disabled": true - }, - { - "key": "resourceBookingId", - "value": "{{resourceBookingId}}", - "disabled": true - }, - { - "key": "resourceBookingIds", - "value": "{{resourceBookingId}},{{resourceBookingIdCreatedByM2M}}", - "disabled": true - }, - { - "key": "paymentStatus", - "value": "pending", - "disabled": true - }, - { - "key": "startDate", - "value": "2021-03-14", - "disabled": true - }, - { - "key": "endDate", - "value": "2021-03-20", - "disabled": true - }, - { - "key": "userHandle", - "value": "pshah_manager", - "disabled": true - }, - { - "key": "projectId", - "value": "16843", - "disabled": true - } - ] - } - }, - "response": [] - }, - { - "name": "search work periods with invalid token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 401', function () {\r", - " pm.response.to.have.status(401);\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer invalid_token" - } - ], - "url": { - "raw": "{{URL}}/work-periods", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods" - ], - "query": [ - { - "key": "page", - "value": "1", - "disabled": true - }, - { - "key": "perPage", - "value": "5", - "disabled": true - }, - { - "key": "sortBy", - "value": "id", - "disabled": true - }, - { - "key": "sortOrder", - "value": "desc", - "disabled": true - }, - { - "key": "resourceBookingId", - "value": "{{resourceBookingId}}", - "disabled": true - }, - { - "key": "resourceBookingIds", - "value": "{{resourceBookingId}},{{resourceBookingIdCreatedByM2M}}", - "disabled": true - }, - { - "key": "paymentStatus", - "value": "pending", - "disabled": true - }, - { - "key": "startDate", - "value": "2021-03-14", - "disabled": true - }, - { - "key": "endDate", - "value": "2021-03-20", - "disabled": true - }, - { - "key": "userHandle", - "value": "pshah_manager", - "disabled": true - }, - { - "key": "projectId", - "value": "111", - "disabled": true - } - ] - } - }, - "response": [] - }, - { - "name": "put work period with booking manager", - "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": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period with m2m update", - "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": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_m2m_update_work_period}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-14\",\r\n \"endDate\": \"2021-03-20\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodIdCreatedByM2M}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodIdCreatedByM2M}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period with connect user", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_connectUser}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-21\",\r\n \"endDate\": \"2021-03-27\",\r\n \"daysWorked\": 3,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"cancelled\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period with member", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_member}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-21\",\r\n \"endDate\": \"2021-03-27\",\r\n \"daysWorked\": 3,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"cancelled\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period with user id not exist", - "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(\"Bad Request\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_userId_not_exist}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-21\",\r\n \"endDate\": \"2021-03-27\",\r\n \"daysWorked\": 3,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"cancelled\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period 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": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer invalid_token" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-21\",\r\n \"endDate\": \"2021-03-27\",\r\n \"daysWorked\": 3,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"cancelled\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period with missing parameter 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.resourceBookingId\\\" is required\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period with missing parameter 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.endDate\\\" is required\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period with missing parameter 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.paymentStatus\\\" is required\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period with invalid parameter 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.resourceBookingId\\\" must be a valid GUID\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"aaa-aaa\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period with invalid parameter 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.startDate\\\" must be in YYYY-MM-DD format\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"07-03-2021\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period with invalid parameter 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(\"startDate should be always Sunday\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-06\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period with invalid parameter 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(\"endDate should be always the next Saturday\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-14\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period with invalid parameter 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.daysWorked\\\" must be a number\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": \"aa\",\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period with invalid parameter 6", - "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(`Key (resource_booking_id, start_date, end_date)=(${pm.environment.get('resourceBookingId')}, 2021-03-14, 2021-03-20) already exists.`)\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-14\",\r\n \"endDate\": \"2021-03-20\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "put work period with invalid parameter 7", - "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.paymentStatus\\\" must be one of [pending, partially-completed, completed, cancelled]\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"paid\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period with booking manager", - "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", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period with m2m update", - "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", - "type": "text", - "value": "Bearer {{token_m2m_update_work_period}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-14\",\r\n \"endDate\": \"2021-03-20\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodIdCreatedByM2M}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodIdCreatedByM2M}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period with connect user", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_connectUser}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-21\",\r\n \"endDate\": \"2021-03-27\",\r\n \"daysWorked\": 3,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"cancelled\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period with member", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_member}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-21\",\r\n \"endDate\": \"2021-03-27\",\r\n \"daysWorked\": 3,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"cancelled\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period with user id not exist", - "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(\"Bad Request\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_userId_not_exist}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-21\",\r\n \"endDate\": \"2021-03-27\",\r\n \"daysWorked\": 3,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"cancelled\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period 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": "PATCH", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer invalid_token" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-21\",\r\n \"endDate\": \"2021-03-27\",\r\n \"daysWorked\": 3,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"cancelled\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period with invalid parameter 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.resourceBookingId\\\" must be a valid GUID\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"aaa-aaa\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period with invalid parameter 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.startDate\\\" must be in YYYY-MM-DD format\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"07-03-2021\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period with invalid parameter 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(\"startDate should be always Sunday\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-06\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period with invalid parameter 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(\"endDate should be always the next Saturday\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-14\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period with invalid parameter 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.daysWorked\\\" must be a number\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": \"aa\",\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period with invalid parameter 6", - "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(`Key (resource_booking_id, start_date, end_date)=(${pm.environment.get('resourceBookingId')}, 2021-03-14, 2021-03-20) already exists.`)\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-14\",\r\n \"endDate\": \"2021-03-20\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period with invalid parameter 7", - "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.paymentStatus\\\" must be one of [pending, partially-completed, completed, cancelled]\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"paid\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "delete work period with member", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_member}}" - } - ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "delete work period with connect user", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_connectUser}}" - } - ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "delete work period with booking manager", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 204', function () {\r", - " pm.response.to.have.status(204);\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "delete work period with m2m delete", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 204', function () {\r", - " pm.response.to.have.status(204);\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_m2m_delete_work_period}}" - } - ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodIdCreatedByM2M}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodIdCreatedByM2M}}" - ] - } - }, - "response": [] - }, - { - "name": "delete work period 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": "DELETE", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer invalid_token" - } - ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - }, - { - "name": "delete work period not found", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 404', function () {\r", - " pm.response.to.have.status(404);\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods/{{workPeriodId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods", - "{{workPeriodId}}" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Work Period Payments", - "item": [ - { - "name": "Before Test", - "item": [ - { - "name": "create job", - "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(\"jobId\", response.id);\r", - " }\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token_bookingManager}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/jobs", - "host": [ - "{{URL}}" - ], - "path": [ - "jobs" - ] - } - }, - "response": [] - }, - { - "name": "create resource booking", - "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(\"resourceBookingId\", response.id);\r", - " }\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"projectId\": {{project_id_17234}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27\",\r\n \"endDate\": \"2020-10-27\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"billingAccountId\": 80000071\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/resourceBookings", - "host": [ - "{{URL}}" - ], - "path": [ - "resourceBookings" - ] - } - }, - "response": [] - }, - { - "name": "create work period", - "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(\"workPeriodId\", response.id);\r", - " }\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-07\",\r\n \"endDate\": \"2021-03-13\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods" - ] - } - }, - "response": [] - }, - { - "name": "create work period2", - "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(\"workPeriodId2\", response.id);\r", - " }\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-14\",\r\n \"endDate\": \"2021-03-20\",\r\n \"daysWorked\": 2,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"pending\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods" - ] - } - }, - "response": [] - }, - { - "name": "create work period 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(\"workPeriodIdCreatedByM2M\", response.id);\r", - " }\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_m2m_create_work_period}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"resourceBookingId\": \"{{resourceBookingId}}\",\r\n \"startDate\": \"2021-03-14\",\r\n \"endDate\": \"2021-03-20\",\r\n \"daysWorked\": 3,\r\n \"memberRate\": 13.13,\r\n \"customerRate\": 13.13,\r\n \"paymentStatus\": \"cancelled\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-periods", - "host": [ - "{{URL}}" - ], - "path": [ - "work-periods" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "create work period payment with boooking manager", - "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(\"workPeriodPaymentId\", response.id);\r", - " }\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-period-payments", - "host": [ - "{{URL}}" - ], - "path": [ - "work-period-payments" - ] - } - }, - "response": [] - }, - { - "name": "create multiple work period payments with boooking manager", - "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_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "[{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600\r\n},{\r\n \"workPeriodId\": \"{{workPeriodId2}}\",\r\n \"amount\": 900\r\n}]", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-period-payments", - "host": [ - "{{URL}}" - ], - "path": [ - "work-period-payments" - ] - } - }, - "response": [] - }, - { - "name": "create query work period payments with boooking manager", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 200', function () {\r", - " pm.response.to.have.status(200);\r", + "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.daysWorked\\\" must be a number\")\r", "});" ], "type": "text/javascript" @@ -17163,7 +14865,7 @@ } ], "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Authorization", @@ -17173,7 +14875,7 @@ ], "body": { "mode": "raw", - "raw": "{\"query\": { \"workPeriods.paymentStatus\": \"pending\" } }", + "raw": "{\r\n \"daysWorked\": \"two\"\r\n}", "options": { "raw": { "language": "json" @@ -17181,31 +14883,29 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments/query", + "raw": "{{URL}}/work-periods/{{workPeriodId-1}}", "host": [ "{{URL}}" ], "path": [ - "work-period-payments", - "query" + "work-periods", + "{{workPeriodId-1}}" ] } }, "response": [] }, { - "name": "create work period payment with m2m create", + "name": "patch work period with invalid parameter 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(\"workPeriodPaymentIdCreatedByM2M\", response.id);\r", - " }\r", + "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.daysWorked\\\" must be greater than or equal to 0\")\r", "});" ], "type": "text/javascript" @@ -17213,17 +14913,17 @@ } ], "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_m2m_create_work_period_payment}}" + "value": "Bearer {{token_bookingManager}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodIdCreatedByM2M}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"daysWorked\": -1\r\n}", "options": { "raw": { "language": "json" @@ -17231,28 +14931,29 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments", + "raw": "{{URL}}/work-periods/{{workPeriodId-1}}", "host": [ "{{URL}}" ], "path": [ - "work-period-payments" + "work-periods", + "{{workPeriodId-1}}" ] } }, "response": [] }, { - "name": "create work period payment with connect user", + "name": "patch work period with invalid parameter 3", "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 400', function () {\r", + " pm.response.to.have.status(400);\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(\"\\\"data.daysWorked\\\" must be less than or equal to 5\")\r", "});" ], "type": "text/javascript" @@ -17260,17 +14961,17 @@ } ], "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_connectUser}}" + "value": "Bearer {{token_bookingManager}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"daysWorked\": 6\r\n}", "options": { "raw": { "language": "json" @@ -17278,28 +14979,29 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments", + "raw": "{{URL}}/work-periods/{{workPeriodId-1}}", "host": [ "{{URL}}" ], "path": [ - "work-period-payments" + "work-periods", + "{{workPeriodId-1}}" ] } }, "response": [] }, { - "name": "create work period payment with member", + "name": "patch work period with invalid parameter 4", "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 400', function () {\r", + " pm.response.to.have.status(400);\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(\"Maximum allowed daysWorked is (4)\")\r", "});" ], "type": "text/javascript" @@ -17307,17 +15009,17 @@ } ], "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_member}}" + "value": "Bearer {{token_bookingManager}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"daysWorked\": 5\r\n}", "options": { "raw": { "language": "json" @@ -17325,19 +15027,20 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments", + "raw": "{{URL}}/work-periods/{{workPeriodId-1}}", "host": [ "{{URL}}" ], "path": [ - "work-period-payments" + "work-periods", + "{{workPeriodId-1}}" ] } }, "response": [] }, { - "name": "create work period payment with user id not exist", + "name": "post process payment for work period 5 days", "event": [ { "listen": "test", @@ -17346,7 +15049,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(\"Bad Request\")\r", + " pm.expect(response.message).to.eq(\"Days cannot be more than not paid days which is 0\")\r", "});" ], "type": "text/javascript" @@ -17359,12 +15062,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_userId_not_exist}}" + "value": "Bearer {{token_bookingManager}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId-1}}\",\r\n \"days\": 5\r\n}", "options": { "raw": { "language": "json" @@ -17384,16 +15087,14 @@ "response": [] }, { - "name": "create work period payment with invalid token", + "name": "post process payment for work period 2 days", "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", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -17406,12 +15107,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer invalid_token" + "value": "Bearer {{token_bookingManager}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId-1}}\",\r\n \"days\": 2\r\n}", "options": { "raw": { "language": "json" @@ -17431,7 +15132,7 @@ "response": [] }, { - "name": "create work period payment with missing workPeriodId", + "name": "patch work period with invalid parameter 5", "event": [ { "listen": "test", @@ -17440,7 +15141,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(\"\\\"workPeriodPayment.workPeriodId\\\" is required\")\r", + " pm.expect(response.message).to.eq(\"Cannot update daysWorked (1) to the value less than daysPaid (2)\")\r", "});" ], "type": "text/javascript" @@ -17448,7 +15149,7 @@ } ], "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Authorization", @@ -17458,7 +15159,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"daysWorked\": 1\r\n}", "options": { "raw": { "language": "json" @@ -17466,28 +15167,29 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments", + "raw": "{{URL}}/work-periods/{{workPeriodId-1}}", "host": [ "{{URL}}" ], "path": [ - "work-period-payments" + "work-periods", + "{{workPeriodId-1}}" ] } }, "response": [] }, { - "name": "create work period payment with invalid workPeriodId 1", + "name": "delete work period", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 400', function () {\r", - " pm.response.to.have.status(400);\r", + "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(\"\\\"workPeriodPayment.workPeriodId\\\" must be a valid GUID\")\r", + " pm.expect(response.message).to.eq(\"The requested resource cannot be found.\")\r", "});" ], "type": "text/javascript" @@ -17495,7 +15197,7 @@ } ], "request": { - "method": "POST", + "method": "DELETE", "header": [ { "key": "Authorization", @@ -17505,7 +15207,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"aaa-bb-c\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "", "options": { "raw": { "language": "json" @@ -17513,28 +15215,199 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments", + "raw": "{{URL}}/work-periods/{{workPeriodId-1}}", "host": [ "{{URL}}" ], "path": [ - "work-period-payments" + "work-periods", + "{{workPeriodId-1}}" ] } }, "response": [] + } + ] + }, + { + "name": "Work Period Payments", + "item": [ + { + "name": "Before Test", + "item": [ + { + "name": "create job", + "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(\"jobId\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_bookingManager}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\",\n \"minSalary\": 100,\n \"maxSalary\": 200,\n \"hoursPerWeek\": 20,\n \"jobLocation\": \"Any location\",\n \"jobTimezone\": \"GMT\",\n \"currency\": \"USD\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/jobs", + "host": [ + "{{URL}}" + ], + "path": [ + "jobs" + ] + } + }, + "response": [] + }, + { + "name": "create resource booking", + "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(\"resourceBookingId\", response.id);\r", + " }\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"projectId\": {{project_id_17234}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27\",\r\n \"endDate\": \"2020-10-27\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"billingAccountId\": 80000071\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/resourceBookings", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ] + } + }, + "response": [] + }, + { + "name": "search work periods", + "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.environment.set(\"workPeriodId-1\", response[0].id);\r", + " pm.environment.set(\"workPeriodId-2\", response[1].id);\r", + " pm.environment.set(\"workPeriodId-3\", response[2].id);\r", + " pm.environment.set(\"workPeriodId-4\", response[3].id);\r", + " pm.environment.set(\"workPeriodId-5\", response[4].id);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/work-periods?sortBy=startDate&sortOrder=asc&resourceBookingId={{resourceBookingId}}&perPage=5", + "host": [ + "{{URL}}" + ], + "path": [ + "work-periods" + ], + "query": [ + { + "key": "sortBy", + "value": "startDate" + }, + { + "key": "sortOrder", + "value": "asc" + }, + { + "key": "resourceBookingId", + "value": "{{resourceBookingId}}" + }, + { + "key": "perPage", + "value": "5" + } + ] + } + }, + "response": [] + } + ] }, { - "name": "create work period payment with invalid workPeriodId 2", + "name": "create work period payment with boooking manager", "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(\"\\\"workPeriodPayment.workPeriodId\\\" must be a string\")\r", + "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(\"workPeriodPaymentId\", response.id);\r", + " }\r", "});" ], "type": "text/javascript" @@ -17552,7 +15425,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": 123,\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId-1}}\"\r\n}", "options": { "raw": { "language": "json" @@ -17572,16 +15445,19 @@ "response": [] }, { - "name": "create work period payment with invalid amount 1", + "name": "create multiple work period payments with boooking manager", "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(\"\\\"workPeriodPayment.amount\\\" must be a number\")\r", + "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(\"workPeriodPaymentId-2\", response[0].id);\r", + " pm.environment.set(\"workPeriodPaymentId-3\", response[0].id);\r", + " }\r", "});" ], "type": "text/javascript" @@ -17599,7 +15475,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": \"abc\",\r\n \"status\": \"completed\"\r\n}", + "raw": "[{\r\n \"workPeriodId\": \"{{workPeriodId-2}}\",\r\n \"days\": 3\r\n},{\r\n \"workPeriodId\": \"{{workPeriodId-3}}\",\r\n \"days\": 4\r\n}]", "options": { "raw": { "language": "json" @@ -17619,16 +15495,14 @@ "response": [] }, { - "name": "create work period payment with invalid amount 2", + "name": "create query work period payments with boooking manager", "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(\"\\\"workPeriodPayment.amount\\\" must be greater than 0\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -17646,7 +15520,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 0,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\"query\": { \"workPeriods.paymentStatus\": \"pending\" } }", "options": { "raw": { "language": "json" @@ -17654,28 +15528,31 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments", + "raw": "{{URL}}/work-period-payments/query", "host": [ "{{URL}}" ], "path": [ - "work-period-payments" + "work-period-payments", + "query" ] } }, "response": [] }, { - "name": "create work period payment with invalid status 1", + "name": "create work period payment with m2m create", "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(\"\\\"workPeriodPayment.status\\\" must be one of [completed, cancelled]\")\r", + "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(\"workPeriodPaymentIdCreatedByM2M\", response.id);\r", + " }\r", "});" ], "type": "text/javascript" @@ -17688,12 +15565,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_m2m_create_work_period_payment}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 1200,\r\n \"status\": 123\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId-2}}\"\r\n}", "options": { "raw": { "language": "json" @@ -17713,16 +15590,16 @@ "response": [] }, { - "name": "create work period payment with invalid status 2", + "name": "create work period payment with connect user", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 400', function () {\r", - " pm.response.to.have.status(400);\r", + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.status\\\" must be one of [completed, cancelled]\")\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", "});" ], "type": "text/javascript" @@ -17735,12 +15612,12 @@ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_connectUser}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 1200,\r\n \"status\": \"invalid-status\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId-3}}\"\r\n}", "options": { "raw": { "language": "json" @@ -17760,14 +15637,16 @@ "response": [] }, { - "name": "get work period payment with booking manager", + "name": "create work period payment with member", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 200', function () {\r", - " pm.response.to.have.status(200);\r", + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", "});" ], "type": "text/javascript" @@ -17775,73 +15654,46 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_member}}" } ], - "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-period-payments", - "{{workPeriodPaymentId}}" - ] - } - }, - "response": [] - }, - { - "name": "get work period payment with m2m read", - "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", - "type": "text", - "value": "Bearer {{token_m2m_read_work_period_payment}}" + "body": { + "mode": "raw", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId-3}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } } - ], + }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentIdCreatedByM2M}}", + "raw": "{{URL}}/work-period-payments", "host": [ "{{URL}}" ], "path": [ - "work-period-payments", - "{{workPeriodPaymentIdCreatedByM2M}}" + "work-period-payments" ] } }, "response": [] }, { - "name": "get work period payment with booking manager from db", + "name": "create work period payment with user id not exist", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 200', function () {\r", - " pm.response.to.have.status(200);\r", + "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(\"Bad Request\")\r", "});" ], "type": "text/javascript" @@ -17849,83 +15701,46 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_userId_not_exist}}" } ], - "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}?fromDb=true", - "host": [ - "{{URL}}" - ], - "path": [ - "work-period-payments", - "{{workPeriodPaymentId}}" - ], - "query": [ - { - "key": "fromDb", - "value": "true" + "body": { + "mode": "raw", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId-3}}\"\r\n}", + "options": { + "raw": { + "language": "json" } - ] - } - }, - "response": [] - }, - { - "name": "get work period payment with connect user", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_connectUser}}" } - ], + }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", + "raw": "{{URL}}/work-period-payments", "host": [ "{{URL}}" ], "path": [ - "work-period-payments", - "{{workPeriodPaymentId}}" + "work-period-payments" ] } }, "response": [] }, { - "name": "get work period payment with member", + "name": "create work period payment with invalid token", "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(\"Invalid Token.\")\r", "});" ], "type": "text/javascript" @@ -17933,36 +15748,46 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_member}}" + "value": "Bearer invalid_token" } ], + "body": { + "mode": "raw", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId-3}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", + "raw": "{{URL}}/work-period-payments", "host": [ "{{URL}}" ], "path": [ - "work-period-payments", - "{{workPeriodPaymentId}}" + "work-period-payments" ] } }, "response": [] }, { - "name": "search work period payments with booking manager", + "name": "create work period payment with missing workPeriodId", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 200', function () {\r", - " pm.response.to.have.status(200);\r", + "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(\"\\\"workPeriodPayment.workPeriodId\\\" is required\")\r", "});" ], "type": "text/javascript" @@ -17970,7 +15795,7 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ { "key": "Authorization", @@ -17978,64 +15803,38 @@ "value": "Bearer {{token_bookingManager}}" } ], - "url": { - "raw": "{{URL}}/work-period-payments", - "host": [ - "{{URL}}" - ], - "path": [ - "work-period-payments" - ], - "query": [ - { - "key": "page", - "value": "1", - "disabled": true - }, - { - "key": "perPage", - "value": "50", - "disabled": true - }, - { - "key": "sortBy", - "value": "status", - "disabled": true - }, - { - "key": "sortOrder", - "value": "desc", - "disabled": true - }, - { - "key": "workPeriodId", - "value": "{{workPeriodId}}", - "disabled": true - }, - { - "key": "workPeriodIds", - "value": "{{workPeriodId}},{{workPeriodIdCreatedByM2M}}", - "disabled": true - }, - { - "key": "status", - "value": "completed", - "disabled": true + "body": { + "mode": "raw", + "raw": "{\r\n\r\n}", + "options": { + "raw": { + "language": "json" } + } + }, + "url": { + "raw": "{{URL}}/work-period-payments", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments" ] } }, "response": [] }, { - "name": "search work period payments with m2m all", + "name": "create work period payment with invalid workPeriodId 1", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 200', function () {\r", - " pm.response.to.have.status(200);\r", + "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(\"\\\"workPeriodPayment.workPeriodId\\\" must be a valid GUID\")\r", "});" ], "type": "text/javascript" @@ -18043,14 +15842,23 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_m2m_all_work_period_payment}}" + "value": "Bearer {{token_bookingManager}}" } ], + "body": { + "mode": "raw", + "raw": "{\r\n \"workPeriodId\": \"aaa-bb-c\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { "raw": "{{URL}}/work-period-payments", "host": [ @@ -18058,59 +15866,22 @@ ], "path": [ "work-period-payments" - ], - "query": [ - { - "key": "page", - "value": "1", - "disabled": true - }, - { - "key": "perPage", - "value": "50", - "disabled": true - }, - { - "key": "sortBy", - "value": "status", - "disabled": true - }, - { - "key": "sortOrder", - "value": "desc", - "disabled": true - }, - { - "key": "workPeriodId", - "value": "{{workPeriodId}}", - "disabled": true - }, - { - "key": "workPeriodIds", - "value": "{{workPeriodId}},{{workPeriodIdCreatedByM2M}}", - "disabled": true - }, - { - "key": "status", - "value": "completed", - "disabled": true - } ] } }, "response": [] }, { - "name": "search work period payments with connect user", + "name": "create work period payment with invalid workPeriodId 2", "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 400', function () {\r", + " pm.response.to.have.status(400);\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(\"\\\"workPeriodPayment.workPeriodId\\\" must be a string\")\r", "});" ], "type": "text/javascript" @@ -18118,14 +15889,23 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_connectUser}}" + "value": "Bearer {{token_bookingManager}}" } ], + "body": { + "mode": "raw", + "raw": "{\r\n \"workPeriodId\": 123\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { "raw": "{{URL}}/work-period-payments", "host": [ @@ -18133,59 +15913,22 @@ ], "path": [ "work-period-payments" - ], - "query": [ - { - "key": "page", - "value": "1", - "disabled": true - }, - { - "key": "perPage", - "value": "50", - "disabled": true - }, - { - "key": "sortBy", - "value": "status", - "disabled": true - }, - { - "key": "sortOrder", - "value": "desc", - "disabled": true - }, - { - "key": "workPeriodId", - "value": "{{workPeriodId}}", - "disabled": true - }, - { - "key": "workPeriodIds", - "value": "{{workPeriodId}},{{workPeriodIdCreatedByM2M}}", - "disabled": true - }, - { - "key": "status", - "value": "completed", - "disabled": true - } ] } }, "response": [] }, { - "name": "search work period payments with member", + "name": "create work period payment with invalid days 1", "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 400', function () {\r", + " pm.response.to.have.status(400);\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(\"Days cannot be more than not paid days which is 0\")\r", "});" ], "type": "text/javascript" @@ -18193,14 +15936,23 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_member}}" + "value": "Bearer {{token_bookingManager}}" } ], + "body": { + "mode": "raw", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId-1}}\",\r\n \"days\": 1\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { "raw": "{{URL}}/work-period-payments", "host": [ @@ -18208,59 +15960,22 @@ ], "path": [ "work-period-payments" - ], - "query": [ - { - "key": "page", - "value": "1", - "disabled": true - }, - { - "key": "perPage", - "value": "50", - "disabled": true - }, - { - "key": "sortBy", - "value": "status", - "disabled": true - }, - { - "key": "sortOrder", - "value": "desc", - "disabled": true - }, - { - "key": "workPeriodId", - "value": "{{workPeriodId}}", - "disabled": true - }, - { - "key": "workPeriodIds", - "value": "{{workPeriodId}},{{workPeriodIdCreatedByM2M}}", - "disabled": true - }, - { - "key": "status", - "value": "completed", - "disabled": true - } ] } }, "response": [] }, { - "name": "search work period payments with invalid token", + "name": "create work period payment with invalid days 2", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 401', function () {\r", - " pm.response.to.have.status(401);\r", + "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 Token.\")\r", + " pm.expect(response.message).to.eq(\"\\\"workPeriodPayment.days\\\" must be less than or equal to 5\")\r", "});" ], "type": "text/javascript" @@ -18268,14 +15983,23 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer invalid_token" + "value": "Bearer {{token_bookingManager}}" } ], + "body": { + "mode": "raw", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId-3}}\",\r\n \"days\": 6\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { "raw": "{{URL}}/work-period-payments", "host": [ @@ -18283,57 +16007,22 @@ ], "path": [ "work-period-payments" - ], - "query": [ - { - "key": "page", - "value": "1", - "disabled": true - }, - { - "key": "perPage", - "value": "50", - "disabled": true - }, - { - "key": "sortBy", - "value": "status", - "disabled": true - }, - { - "key": "sortOrder", - "value": "desc", - "disabled": true - }, - { - "key": "workPeriodId", - "value": "{{workPeriodId}}", - "disabled": true - }, - { - "key": "workPeriodIds", - "value": "{{workPeriodId}},{{workPeriodIdCreatedByM2M}}", - "disabled": true - }, - { - "key": "status", - "value": "completed", - "disabled": true - } ] } }, "response": [] }, { - "name": "put work period payment with boooking manager", + "name": "create work period payment with invalid days 2 Copy", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 200', function () {\r", - " pm.response.to.have.status(200);\r", + "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(\"Days cannot be more than not paid days which is 1\")\r", "});" ], "type": "text/javascript" @@ -18341,7 +16030,7 @@ } ], "request": { - "method": "PUT", + "method": "POST", "header": [ { "key": "Authorization", @@ -18351,7 +16040,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 1600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId-3}}\",\r\n \"days\": 3\r\n}", "options": { "raw": { "language": "json" @@ -18359,27 +16048,28 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", + "raw": "{{URL}}/work-period-payments", "host": [ "{{URL}}" ], "path": [ - "work-period-payments", - "{{workPeriodPaymentId}}" + "work-period-payments" ] } }, "response": [] }, { - "name": "put work period payment with m2m create", + "name": "create work period payment with completed status", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 200', function () {\r", - " pm.response.to.have.status(200);\r", + "pm.test('Status code is 409', function () {\r", + " pm.response.to.have.status(409);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"There are no days to pay for WorkPeriod: \"+pm.environment.get('workPeriodId-2'))\r", "});" ], "type": "text/javascript" @@ -18387,17 +16077,17 @@ } ], "request": { - "method": "PUT", + "method": "POST", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_m2m_update_work_period_payment}}" + "value": "Bearer {{token_bookingManager}}" } ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodIdCreatedByM2M}}\",\r\n \"amount\": 1600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId-2}}\"\r\n}", "options": { "raw": { "language": "json" @@ -18405,29 +16095,26 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentIdCreatedByM2M}}", + "raw": "{{URL}}/work-period-payments", "host": [ "{{URL}}" ], "path": [ - "work-period-payments", - "{{workPeriodPaymentIdCreatedByM2M}}" + "work-period-payments" ] } }, "response": [] }, { - "name": "put work period payment with connect user", + "name": "get work period payment with booking manager", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -18435,23 +16122,14 @@ } ], "request": { - "method": "PUT", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_connectUser}}" + "value": "Bearer {{token_bookingManager}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", "host": [ @@ -18466,16 +16144,14 @@ "response": [] }, { - "name": "put work period payment with member", + "name": "get work period payment with m2m read", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 403', function () {\r", - " pm.response.to.have.status(403);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -18483,47 +16159,36 @@ } ], "request": { - "method": "PUT", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_member}}" + "value": "Bearer {{token_m2m_read_work_period_payment}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", + "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentIdCreatedByM2M}}", "host": [ "{{URL}}" ], "path": [ "work-period-payments", - "{{workPeriodPaymentId}}" + "{{workPeriodPaymentIdCreatedByM2M}}" ] } }, "response": [] }, { - "name": "put work period payment with user id not exist", + "name": "get work period payment with booking manager from db", "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(\"Bad Request\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -18531,47 +16196,44 @@ } ], "request": { - "method": "PUT", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_userId_not_exist}}" + "value": "Bearer {{token_bookingManager}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", + "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}?fromDb=true", "host": [ "{{URL}}" ], "path": [ "work-period-payments", "{{workPeriodPaymentId}}" + ], + "query": [ + { + "key": "fromDb", + "value": "true" + } ] } }, "response": [] }, { - "name": "put work period payment with invalid token", + "name": "get work period payment with connect user", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 401', function () {\r", - " pm.response.to.have.status(401);\r", + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", "});" ], "type": "text/javascript" @@ -18579,23 +16241,14 @@ } ], "request": { - "method": "PUT", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer invalid_token" + "value": "Bearer {{token_connectUser}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", "host": [ @@ -18610,16 +16263,16 @@ "response": [] }, { - "name": "put work period payment with missing workPeriodId", + "name": "get work period payment with member", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 400', function () {\r", - " pm.response.to.have.status(400);\r", + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"\\\"data.workPeriodId\\\" is required\")\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", "});" ], "type": "text/javascript" @@ -18627,23 +16280,14 @@ } ], "request": { - "method": "PUT", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_member}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", "host": [ @@ -18658,16 +16302,14 @@ "response": [] }, { - "name": "put work period payment with invalid workPeriodId 1", + "name": "search work period payments with booking manager", "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.workPeriodId\\\" must be a valid GUID\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -18675,7 +16317,7 @@ } ], "request": { - "method": "PUT", + "method": "GET", "header": [ { "key": "Authorization", @@ -18683,39 +16325,64 @@ "value": "Bearer {{token_bookingManager}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"aaa-bb-c\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", + "raw": "{{URL}}/work-period-payments", "host": [ "{{URL}}" ], "path": [ - "work-period-payments", - "{{workPeriodPaymentId}}" + "work-period-payments" + ], + "query": [ + { + "key": "page", + "value": "1", + "disabled": true + }, + { + "key": "perPage", + "value": "50", + "disabled": true + }, + { + "key": "sortBy", + "value": "status", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "workPeriodId", + "value": "{{workPeriodId}}", + "disabled": true + }, + { + "key": "workPeriodIds", + "value": "{{workPeriodId}},{{workPeriodIdCreatedByM2M}}", + "disabled": true + }, + { + "key": "status", + "value": "completed", + "disabled": true + } ] } }, "response": [] }, { - "name": "put work period payment with invalid workPeriodId 2", + "name": "search work period payments with m2m all", "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.workPeriodId\\\" must be a string\")\r", + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", "});" ], "type": "text/javascript" @@ -18723,47 +16390,74 @@ } ], "request": { - "method": "PUT", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_m2m_all_work_period_payment}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"workPeriodId\": 123,\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", + "raw": "{{URL}}/work-period-payments", "host": [ "{{URL}}" ], "path": [ - "work-period-payments", - "{{workPeriodPaymentId}}" + "work-period-payments" + ], + "query": [ + { + "key": "page", + "value": "1", + "disabled": true + }, + { + "key": "perPage", + "value": "50", + "disabled": true + }, + { + "key": "sortBy", + "value": "status", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "workPeriodId", + "value": "{{workPeriodId}}", + "disabled": true + }, + { + "key": "workPeriodIds", + "value": "{{workPeriodId}},{{workPeriodIdCreatedByM2M}}", + "disabled": true + }, + { + "key": "status", + "value": "completed", + "disabled": true + } ] } }, "response": [] }, { - "name": "put work period payment with invalid amount 1", + "name": "search work period payments with connect user", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 400', function () {\r", - " pm.response.to.have.status(400);\r", + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"\\\"data.amount\\\" must be a number\")\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", "});" ], "type": "text/javascript" @@ -18771,47 +16465,74 @@ } ], "request": { - "method": "PUT", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_connectUser}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": \"abc\",\r\n \"status\": \"completed\"\r\n}", - "options": { - "raw": { - "language": "json" + "url": { + "raw": "{{URL}}/work-period-payments", + "host": [ + "{{URL}}" + ], + "path": [ + "work-period-payments" + ], + "query": [ + { + "key": "page", + "value": "1", + "disabled": true + }, + { + "key": "perPage", + "value": "50", + "disabled": true + }, + { + "key": "sortBy", + "value": "status", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "workPeriodId", + "value": "{{workPeriodId}}", + "disabled": true + }, + { + "key": "workPeriodIds", + "value": "{{workPeriodId}},{{workPeriodIdCreatedByM2M}}", + "disabled": true + }, + { + "key": "status", + "value": "completed", + "disabled": true } - } - }, - "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-period-payments", - "{{workPeriodPaymentId}}" ] } }, "response": [] }, { - "name": "put work period payment with invalid amount 2", + "name": "search work period payments with member", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 400', function () {\r", - " pm.response.to.have.status(400);\r", + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"\\\"data.amount\\\" must be greater than 0\")\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", "});" ], "type": "text/javascript" @@ -18819,47 +16540,74 @@ } ], "request": { - "method": "PUT", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer {{token_member}}" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 0,\r\n \"status\": \"completed\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", + "raw": "{{URL}}/work-period-payments", "host": [ "{{URL}}" ], "path": [ - "work-period-payments", - "{{workPeriodPaymentId}}" + "work-period-payments" + ], + "query": [ + { + "key": "page", + "value": "1", + "disabled": true + }, + { + "key": "perPage", + "value": "50", + "disabled": true + }, + { + "key": "sortBy", + "value": "status", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "workPeriodId", + "value": "{{workPeriodId}}", + "disabled": true + }, + { + "key": "workPeriodIds", + "value": "{{workPeriodId}},{{workPeriodIdCreatedByM2M}}", + "disabled": true + }, + { + "key": "status", + "value": "completed", + "disabled": true + } ] } }, "response": [] }, { - "name": "put work period payment with invalid status 1", + "name": "search work period payments with invalid token", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 400', function () {\r", - " pm.response.to.have.status(400);\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(\"\\\"data.status\\\" must be one of [completed, cancelled]\")\r", + " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", "});" ], "type": "text/javascript" @@ -18867,47 +16615,74 @@ } ], "request": { - "method": "PUT", + "method": "GET", "header": [ { "key": "Authorization", "type": "text", - "value": "Bearer {{token_bookingManager}}" + "value": "Bearer invalid_token" } ], - "body": { - "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 1200,\r\n \"status\": 123\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", + "raw": "{{URL}}/work-period-payments", "host": [ "{{URL}}" ], "path": [ - "work-period-payments", - "{{workPeriodPaymentId}}" + "work-period-payments" + ], + "query": [ + { + "key": "page", + "value": "1", + "disabled": true + }, + { + "key": "perPage", + "value": "50", + "disabled": true + }, + { + "key": "sortBy", + "value": "status", + "disabled": true + }, + { + "key": "sortOrder", + "value": "desc", + "disabled": true + }, + { + "key": "workPeriodId", + "value": "{{workPeriodId}}", + "disabled": true + }, + { + "key": "workPeriodIds", + "value": "{{workPeriodId}},{{workPeriodIdCreatedByM2M}}", + "disabled": true + }, + { + "key": "status", + "value": "completed", + "disabled": true + } ] } }, "response": [] }, { - "name": "put work period payment with invalid status 2", + "name": "put work period payment", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test('Status code is 400', function () {\r", - " pm.response.to.have.status(400);\r", + "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(\"\\\"data.status\\\" must be one of [completed, cancelled]\")\r", + " pm.expect(response.message).to.eq(\"The requested resource cannot be found.\")\r", "});" ], "type": "text/javascript" @@ -18925,7 +16700,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 1200,\r\n \"status\": \"invalid-status\"\r\n}", + "raw": "{\r\n \"status\": \"completed\"\r\n}", "options": { "raw": { "language": "json" @@ -18971,7 +16746,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", + "raw": "{\r\n \"status\": \"cancelled\"\r\n}", "options": { "raw": { "language": "json" @@ -19017,7 +16792,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodIdCreatedByM2M}}\",\r\n \"amount\": 450,\r\n \"status\": \"cancelled\"\r\n}", + "raw": "{\r\n \"status\": \"cancelled\"\r\n}", "options": { "raw": { "language": "json" @@ -19065,7 +16840,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"status\": \"cancelled\"\r\n}", "options": { "raw": { "language": "json" @@ -19113,7 +16888,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"status\": \"cancelled\"\r\n}", "options": { "raw": { "language": "json" @@ -19161,7 +16936,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"status\": \"cancelled\"\r\n}", "options": { "raw": { "language": "json" @@ -19209,103 +16984,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-period-payments", - "{{workPeriodPaymentId}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period payment with invalid workPeriodId 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.workPeriodId\\\" must be a valid GUID\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"aaa-bb-c\",\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", - "host": [ - "{{URL}}" - ], - "path": [ - "work-period-payments", - "{{workPeriodPaymentId}}" - ] - } - }, - "response": [] - }, - { - "name": "patch work period payment with invalid workPeriodId 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.workPeriodId\\\" must be a string\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"workPeriodId\": 123,\r\n \"amount\": 600,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"status\": \"cancelled\"\r\n}", "options": { "raw": { "language": "json" @@ -19326,7 +17005,7 @@ "response": [] }, { - "name": "patch work period payment with invalid amount 1", + "name": "patch work period payment with invalid status 1", "event": [ { "listen": "test", @@ -19335,7 +17014,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.amount\\\" must be a number\")\r", + " pm.expect(response.message).to.eq(\"\\\"data.status\\\" must be one of [scheduled, cancelled]\")\r", "});" ], "type": "text/javascript" @@ -19353,7 +17032,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": \"abc\",\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"status\": 123\r\n}", "options": { "raw": { "language": "json" @@ -19374,7 +17053,7 @@ "response": [] }, { - "name": "patch work period payment with invalid amount 2", + "name": "patch work period payment with invalid status 2", "event": [ { "listen": "test", @@ -19383,7 +17062,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.amount\\\" must be greater than 0\")\r", + " pm.expect(response.message).to.eq(\"\\\"data.status\\\" must be one of [scheduled, cancelled]\")\r", "});" ], "type": "text/javascript" @@ -19401,7 +17080,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 0,\r\n \"status\": \"completed\"\r\n}", + "raw": "{\r\n \"status\": \"invalid-status\"\r\n}", "options": { "raw": { "language": "json" @@ -19422,7 +17101,7 @@ "response": [] }, { - "name": "patch work period payment with invalid status 1", + "name": "patch work period payment with invalid status 3", "event": [ { "listen": "test", @@ -19431,7 +17110,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.status\\\" must be one of [completed, cancelled]\")\r", + " pm.expect(response.message).to.eq(\"You cannot schedule a WorkPeriodPayment which is cancelled\")\r", "});" ], "type": "text/javascript" @@ -19449,7 +17128,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 1200,\r\n \"status\": 123\r\n}", + "raw": "{\r\n \"status\": \"scheduled\"\r\n}", "options": { "raw": { "language": "json" @@ -19470,7 +17149,7 @@ "response": [] }, { - "name": "patch work period payment with invalid status 2", + "name": "patch work period payment with invalid status 4", "event": [ { "listen": "test", @@ -19479,7 +17158,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.status\\\" must be one of [completed, cancelled]\")\r", + " pm.expect(response.message).to.eq(\"You cannot schedule a WorkPeriodPayment which is completed\")\r", "});" ], "type": "text/javascript" @@ -19497,7 +17176,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"workPeriodId\": \"{{workPeriodId}}\",\r\n \"amount\": 1200,\r\n \"status\": \"invalid-status\"\r\n}", + "raw": "{\r\n \"status\": \"scheduled\"\r\n}", "options": { "raw": { "language": "json" @@ -19505,13 +17184,13 @@ } }, "url": { - "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId}}", + "raw": "{{URL}}/work-period-payments/{{workPeriodPaymentId-2}}", "host": [ "{{URL}}" ], "path": [ "work-period-payments", - "{{workPeriodPaymentId}}" + "{{workPeriodPaymentId-2}}" ] } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index cab269c9..21f6e9cf 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -174,7 +174,7 @@ paths: required: false schema: type: string - enum: ["hourly", "daily", "weekly", "monthly","annual"] + enum: ["hourly", "daily", "weekly", "monthly", "annual"] description: The rate type. - in: query name: status @@ -1509,9 +1509,11 @@ paths: - type: array items: type: string - enum: ["pending", "partially-completed", "completed", "cancelled"] + enum: + ["pending", "partially-completed", "completed", "cancelled"] - type: string - enum: ["pending", "partially-completed", "completed", "cancelled"] + enum: + ["pending", "partially-completed", "completed", "cancelled"] description: comma separated payment status. - in: query name: workPeriods.startDate @@ -1837,57 +1839,6 @@ paths: schema: $ref: "#/components/schemas/Error" /work-periods: - post: - tags: - - WorkPeriods - description: | - Create Work Period. - - **Authorization** Topcoder token with write Work period scope is allowed - security: - - bearerAuth: [] - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/WorkPeriodRequestBody" - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/WorkPeriod" - "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" get: tags: - WorkPeriods @@ -1965,9 +1916,25 @@ paths: - type: array items: type: string - enum: ["pending", "partially-completed", "completed", "cancelled"] + enum: + [ + "pending", + "partially-completed", + "completed", + "in-progress", + "failed", + "noDays", + ] - type: string - enum: ["pending", "partially-completed", "completed", "cancelled"] + enum: + [ + "pending", + "partially-completed", + "completed", + "in-progress", + "failed", + "noDays", + ] description: comma separated payment status. - in: query name: startDate @@ -2121,61 +2088,11 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - delete: - tags: - - WorkPeriods - description: | - Delete the work period. - - **Authorization** Topcoder token with delete work period scope is allowed - security: - - bearerAuth: [] - parameters: - - in: path - name: id - description: The id of work period. - required: true - schema: - type: string - format: uuid - responses: - "204": - description: OK - "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" - put: + patch: tags: - WorkPeriods description: | - Update the work period. + Partial Update work period. **Authorization** Topcoder token with update work period scope is allowed security: @@ -2192,7 +2109,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/WorkPeriodRequestBody" + $ref: "#/components/schemas/WorkPeriodPatchRequestBody" responses: "200": description: OK @@ -2224,61 +2141,8 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - "500": - description: Internal Server Error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - patch: - tags: - - WorkPeriods - description: | - Partial Update work period. - - **Authorization** Topcoder token with update work period scope is allowed - security: - - bearerAuth: [] - parameters: - - in: path - name: id - description: The id of work period. - required: true - schema: - type: string - format: uuid - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/WorkPeriodPatchRequestBody" - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/WorkPeriod" - "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 + "409": + description: Conflict content: application/json: schema: @@ -2345,6 +2209,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "409": + description: Conflict + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "500": description: Internal Server Error content: @@ -2413,7 +2283,8 @@ paths: required: false schema: type: string - enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"] + enum: + ["completed", "scheduled", "in-progress", "failed", "cancelled"] description: The payment status. responses: "200": @@ -2592,65 +2463,6 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - put: - tags: - - WorkPeriodPayments - description: | - Update the work period payment. - - **Authorization** Topcoder token with update work period payment scope is allowed - security: - - bearerAuth: [] - parameters: - - in: path - name: id - description: The id of work period payment. - required: true - schema: - type: string - format: uuid - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/WorkPeriodPaymentRequestBody" - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/WorkPeriodPayment" - "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" patch: tags: - WorkPeriodPayments @@ -4670,6 +4482,9 @@ components: - projectId - startDate - endDate + - daysWorked + - daysPaid + - paymentTotal - paymentStatus - createdAt - createdBy @@ -4702,21 +4517,33 @@ components: description: "The end date of work period. Should be always Saturday." daysWorked: type: integer + minimum: 0 + maximum: 5 example: 2 description: "The count of the days worked for that work period." - memberRate: + daysPaid: type: integer - format: float - example: 13.13 - description: "The member rate." - customerRate: + minimum: 0 + maximum: 5 + format: integer + example: 1 + description: "The count of paid days for that work period" + paymentTotal: type: integer format: float example: 13.13 - description: "The customer rate." + description: "The total paid amount" paymentStatus: type: string - enum: ["pending", "partially-completed", "completed", "cancelled"] + enum: + [ + "pending", + "partially-completed", + "completed", + "in-progress", + "failed", + "noDays", + ] description: "The payment status." payments: type: array @@ -4739,86 +4566,21 @@ components: type: string format: uuid description: "The user Id who updated the work period last time.(Will get the user info from the token)" - WorkPeriodRequestBody: - required: - - resourceBookingId - - startDate - - endDate - - paymentStatus - properties: - resourceBookingId: - type: string - format: uuid - description: "The resource booking id." - startDate: - type: string - format: date - example: "2021-03-07" - description: "The start date of work period. Should be always Sunday." - endDate: - type: string - format: date - example: "2021-03-13" - description: "The end date of work period. Should be always Saturday." - daysWorked: - type: integer - example: 2 - description: "The count of the days worked for that work period." - memberRate: - type: integer - format: float - example: 13.13 - description: "The member rate." - customerRate: - type: integer - format: float - example: 13.13 - description: "The customer rate." - paymentStatus: - type: string - enum: ["pending", "partially-completed", "completed", "cancelled"] - description: "The payment status." WorkPeriodPatchRequestBody: properties: - resourceBookingId: - type: string - format: uuid - description: "The resource booking id." - startDate: - type: string - format: date - example: "2021-03-07" - description: "The start date of work period. Should be always Sunday." - endDate: - type: string - format: date - example: "2021-03-13" - description: "The end date of work period. Should be always Saturday." daysWorked: type: integer example: 2 description: "The count of the days worked for that work period." - memberRate: - type: integer - format: float - example: 13.13 - description: "The member rate." - customerRate: - type: integer - format: float - example: 13.13 - description: "The customer rate." - paymentStatus: - type: string - enum: ["pending", "partially-completed", "completed", "cancelled"] - description: "The payment status." WorkPeriodPayment: required: - id - workPeriodId - - challengeId + - memberRate + - days - amount - status + - billingAccountId - createdAt - createdBy properties: @@ -4834,6 +4596,22 @@ components: type: string format: uuid description: "The challenge id." + memberRate: + type: number + format: float + example: 20.5 + description: "The member rate" + customerRate: + type: number + format: float + example: 15.5 + description: "The customer rate" + days: + type: integer + minimum: 1 + maximum: 5 + example: 3 + description: "The workdays to pay" amount: type: integer example: 2 @@ -4898,22 +4676,6 @@ components: type: integer example: 429 description: "HTTP code of error" - WorkPeriodPaymentRequestBody: - required: - - workPeriodId - properties: - workPeriodId: - type: string - format: uuid - description: "The work period id." - amount: - type: integer - example: 2 - description: "The amount to be paid." - status: - type: string - enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"] - description: "The payment status." WorkPeriodPaymentCreateRequestBody: required: - workPeriodId @@ -4922,10 +4684,12 @@ components: type: string format: uuid description: "The work period id." - amount: + days: type: integer + minimum: 1 + maximum: 5 example: 2 - description: "The amount to be paid." + description: "The workDays to be paid." WorkPeriodPaymentQueryCreateRequestBody: properties: status: @@ -4964,7 +4728,7 @@ components: type: integer workPeriods.paymentStatus: type: string - enum: ["pending", "partially-completed", "completed", "cancelled"] + enum: ["pending", "partially-completed", "failed"] workPeriods.startDate: type: string format: date @@ -4993,17 +4757,9 @@ components: $ref: "#/components/schemas/WorkPeriodPaymentQueryCreateRequestBody" WorkPeriodPaymentPatchRequestBody: properties: - workPeriodId: - type: string - format: uuid - description: "The work period id." - amount: - type: integer - example: 2 - description: "The amount to be paid." status: type: string - enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"] + enum: ["scheduled", "cancelled"] description: "The payment status." CheckRun: type: object diff --git a/migrations/2021-06-14-create-and-populate-work-periods-for-resource-bookings.js b/migrations/2021-06-14-create-and-populate-work-periods-for-resource-bookings.js new file mode 100644 index 00000000..dcda9f36 --- /dev/null +++ b/migrations/2021-06-14-create-and-populate-work-periods-for-resource-bookings.js @@ -0,0 +1,145 @@ +const config = require('config') +const ResourceBooking = require('../src/models').ResourceBooking +const _ = require('lodash') +const helper = require('../src/common/helper') +const { v4: uuid } = require('uuid') + +// maximum start date of resource bookings when populating work periods from existing resource bookings in migration script +const MAX_START_DATE = process.env.MAX_START_DATE || '2100-12-31' +// maximum end date of resource bookings when populating work periods from existing resource bookings in migration script +const MAX_END_DATE = process.env.MAX_END_DATE || '2100-12-31' + +/* + * Populate WorkPeriods for ResourceBookings + */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + const Op = Sequelize.Op + try { + await queryInterface.bulkDelete({ + tableName: 'payment_schedulers', + schema: config.DB_SCHEMA_NAME, + transaction + }) + await queryInterface.bulkDelete({ + tableName: 'work_period_payments', + schema: config.DB_SCHEMA_NAME, + transaction + }) + await queryInterface.bulkDelete({ + tableName: 'work_periods', + schema: config.DB_SCHEMA_NAME, + transaction + }) + await queryInterface.removeColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'member_rate', { transaction }) + await queryInterface.removeColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'customer_rate', { transaction }) + await queryInterface.addColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'days_paid', + { type: Sequelize.INTEGER, allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'payment_total', + { type: Sequelize.FLOAT, allowNull: false }, + { transaction }) + await queryInterface.changeColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'days_worked', + { type: Sequelize.INTEGER, allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'member_rate', + { type: Sequelize.FLOAT, allowNull: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'customer_rate', + { type: Sequelize.FLOAT, allowNull: true }, + { transaction }) + await queryInterface.addColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'days', + { type: Sequelize.INTEGER, allowNull: false }, + { transaction }) + await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'amount', + { type: Sequelize.DOUBLE, allowNull: false }, + { transaction }) + await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'billing_account_id', + { type: Sequelize.BIGINT, allowNull: false }, + { transaction }) + const resourceBookings = await ResourceBooking.findAll({ + where: { + start_date: { [Op.lt]: new Date(MAX_START_DATE) }, + end_date: { [Op.lt]: new Date(MAX_END_DATE) } + } + }) + if (resourceBookings.length === 0) { + return + } + const workPeriodData = [] + for (const rb of resourceBookings) { + if (!_.isNil(rb.startDate) && !_.isNil(rb.endDate)) { + const periods = helper.extractWorkPeriods(rb.startDate, rb.endDate) + const user = await helper.ensureUserById(rb.userId) + _.forEach(periods, period => { + workPeriodData.push({ + id: uuid(), + resource_booking_id: rb.id, + project_id: rb.projectId, + user_handle: user.handle, + start_date: period.startDate, + end_date: period.endDate, + days_worked: period.daysWorked, + days_paid: 0, + payment_total: 0, + payment_status: period.daysWorked === 0 ? 'noDays' : 'pending', + created_by: config.m2m.M2M_AUDIT_USER_ID, + created_at: new Date() + }) + }) + } + } + await queryInterface.bulkInsert({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, workPeriodData, { transaction }) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + }, + down: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.bulkDelete({ + tableName: 'payment_schedulers', + schema: config.DB_SCHEMA_NAME, + transaction + }) + await queryInterface.bulkDelete({ + tableName: 'work_period_payments', + schema: config.DB_SCHEMA_NAME, + transaction + }) + await queryInterface.bulkDelete({ + tableName: 'work_periods', + schema: config.DB_SCHEMA_NAME, + transaction + }) + await queryInterface.removeColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'days_paid', { transaction }) + await queryInterface.removeColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'payment_total', { transaction }) + await queryInterface.addColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'member_rate', + { type: Sequelize.FLOAT, allowNull: true }, + { transaction }) + await queryInterface.addColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'customer_rate', + { type: Sequelize.FLOAT, allowNull: true }, + { transaction }) + await queryInterface.changeColumn({ tableName: 'work_periods', schema: config.DB_SCHEMA_NAME }, 'days_worked', + { type: Sequelize.INTEGER, allowNull: true }, + { transaction }) + await queryInterface.removeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'member_rate', { transaction }) + await queryInterface.removeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'customer_rate', { transaction }) + await queryInterface.removeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'days', { transaction }) + await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'amount', + { type: Sequelize.DOUBLE, allowNull: true }, + { transaction }) + await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'billing_account_id', + { type: Sequelize.BIGINT, allowNull: true }, + { transaction }) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + } +} diff --git a/src/bootstrap.js b/src/bootstrap.js index aebac2c2..7230bedf 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -3,7 +3,7 @@ const Joi = require('joi') const config = require('config') const path = require('path') const _ = require('lodash') -const { Interviews, WorkPeriodPaymentStatus, PaymentProcessingSwitch } = require('../app-constants') +const { Interviews, WorkPeriodPaymentStatus, WorkPeriodPaymentUpdateStatus, PaymentProcessingSwitch } = require('../app-constants') const logger = require('./common/logger') const allowedInterviewStatuses = _.values(Interviews.Status) @@ -17,10 +17,11 @@ Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancel Joi.workload = () => Joi.string().valid('full-time', 'fractional') Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed', 'offered') Joi.title = () => Joi.string().max(128) -Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') +Joi.paymentStatus = () => Joi.string().valid('pending', 'in-progress', 'partially-completed', 'completed', 'failed', 'noDays') Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) Joi.interviewStatus = () => Joi.string().valid(...allowedInterviewStatuses) Joi.workPeriodPaymentStatus = () => Joi.string().valid(..._.values(WorkPeriodPaymentStatus)) +Joi.workPeriodPaymentUpdateStatus = () => Joi.string().valid(..._.values(WorkPeriodPaymentUpdateStatus)) // Empty string is not allowed by Joi by default and must be enabled with allow(''). // See https://joi.dev/api/?v=17.3.0#string fro details why it's like this. // In many cases we would like to allow empty string to make it easier to create UI for editing data. diff --git a/src/common/helper.js b/src/common/helper.js index f4a1aac2..063b0b98 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -191,8 +191,8 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { startDate: { type: 'date', format: 'yyyy-MM-dd' }, endDate: { type: 'date', format: 'yyyy-MM-dd' }, daysWorked: { type: 'integer' }, - memberRate: { type: 'float' }, - customerRate: { type: 'float' }, + daysPaid: { type: 'integer' }, + paymentTotal: { type: 'float' }, paymentStatus: { type: 'keyword' }, payments: { type: 'nested', @@ -200,6 +200,9 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { id: { type: 'keyword' }, workPeriodId: { type: 'keyword' }, challengeId: { type: 'keyword' }, + memberRate: { type: 'float' }, + customerRate: { type: 'float' }, + days: { type: 'integer' }, amount: { type: 'float' }, status: { type: 'keyword' }, statusDetails: { diff --git a/src/controllers/WorkPeriodController.js b/src/controllers/WorkPeriodController.js index 8a54defa..0d0853de 100644 --- a/src/controllers/WorkPeriodController.js +++ b/src/controllers/WorkPeriodController.js @@ -1,7 +1,6 @@ /** * Controller for WorkPeriod endpoints */ -const HttpStatus = require('http-status-codes') const service = require('../services/WorkPeriodService') const helper = require('../common/helper') @@ -14,15 +13,6 @@ async function getWorkPeriod (req, res) { res.send(await service.getWorkPeriod(req.authUser, req.params.id, req.query.fromDb)) } -/** - * Create workPeriod - * @param req the request - * @param res the response - */ -async function createWorkPeriod (req, res) { - res.send(await service.createWorkPeriod(req.authUser, req.body)) -} - /** * Partially update workPeriod by id * @param req the request @@ -32,25 +22,6 @@ async function partiallyUpdateWorkPeriod (req, res) { res.send(await service.partiallyUpdateWorkPeriod(req.authUser, req.params.id, req.body)) } -/** - * Fully update workPeriod by id - * @param req the request - * @param res the response - */ -async function fullyUpdateWorkPeriod (req, res) { - res.send(await service.fullyUpdateWorkPeriod(req.authUser, req.params.id, req.body)) -} - -/** - * Delete workPeriod by id - * @param req the request - * @param res the response - */ -async function deleteWorkPeriod (req, res) { - await service.deleteWorkPeriod(req.authUser, req.params.id) - res.status(HttpStatus.NO_CONTENT).end() -} - /** * Search workPeriods * @param req the request @@ -64,9 +35,6 @@ async function searchWorkPeriods (req, res) { module.exports = { getWorkPeriod, - createWorkPeriod, partiallyUpdateWorkPeriod, - fullyUpdateWorkPeriod, - deleteWorkPeriod, searchWorkPeriods } diff --git a/src/controllers/WorkPeriodPaymentController.js b/src/controllers/WorkPeriodPaymentController.js index 4bba2385..3ac5c2fe 100644 --- a/src/controllers/WorkPeriodPaymentController.js +++ b/src/controllers/WorkPeriodPaymentController.js @@ -31,15 +31,6 @@ async function partiallyUpdateWorkPeriodPayment (req, res) { res.send(await service.partiallyUpdateWorkPeriodPayment(req.authUser, req.params.id, req.body)) } -/** - * Fully update workPeriodPayment by id - * @param req the request - * @param res the response - */ -async function fullyUpdateWorkPeriodPayment (req, res) { - res.send(await service.fullyUpdateWorkPeriodPayment(req.authUser, req.params.id, req.body)) -} - /** * Search workPeriodPayments * @param req the request @@ -65,6 +56,5 @@ module.exports = { createWorkPeriodPayment, createQueryWorkPeriodPayments, partiallyUpdateWorkPeriodPayment, - fullyUpdateWorkPeriodPayment, searchWorkPeriodPayments } diff --git a/src/eventHandlers/ResourceBookingEventHandler.js b/src/eventHandlers/ResourceBookingEventHandler.js index c82c4e35..96a43fec 100644 --- a/src/eventHandlers/ResourceBookingEventHandler.js +++ b/src/eventHandlers/ResourceBookingEventHandler.js @@ -185,8 +185,9 @@ async function updateWorkPeriods (payload) { if (payload.options.oldValue.startDate !== payload.value.startDate) { const firstWeek = _.minBy(IntersectedWorkPeriods, 'startDate') const originalFirstWeek = _.find(workPeriods, ['startDate', firstWeek.startDate]) - // recalculate daysWorked for the first week of existent workPeriods - if (firstWeek.startDate === _.minBy(workPeriods, 'startDate').startDate) { + const existentFirstWeek = _.minBy(workPeriods, 'startDate') + // recalculate daysWorked for the first week of existent workPeriods and daysWorked have changed + if (firstWeek.startDate === existentFirstWeek.startDate && firstWeek.daysWorked !== existentFirstWeek.daysWorked) { workPeriodsToUpdate.push(_.assign(firstWeek, { id: originalFirstWeek.id })) // if first of intersected workPeriods is not the first one of existent workPeriods // we only check if it's daysWorked exceeds the possible maximum @@ -197,8 +198,9 @@ async function updateWorkPeriods (payload) { if (payload.options.oldValue.endDate !== payload.value.endDate) { const lastWeek = _.maxBy(IntersectedWorkPeriods, 'startDate') const originalLastWeek = _.find(workPeriods, ['startDate', lastWeek.startDate]) - // recalculate daysWorked for the last week of existent workPeriods - if (lastWeek.startDate === _.maxBy(workPeriods, 'startDate').startDate) { + const existentLastWeek = _.maxBy(workPeriods, 'startDate') + // recalculate daysWorked for the last week of existent workPeriods and daysWorked have changed + if (lastWeek.startDate === existentLastWeek.startDate && lastWeek.daysWorked !== existentLastWeek.daysWorked) { workPeriodsToUpdate.push(_.assign(lastWeek, { id: originalLastWeek.id })) // if last of intersected workPeriods is not the last one of existent workPeriods // we only check if it's daysWorked exceeds the possible maximum @@ -287,13 +289,13 @@ async function deleteWorkPeriods (payload) { */ async function _createWorkPeriods (periods, resourceBookingId) { for (const period of periods) { - await WorkPeriodService.createWorkPeriod(helper.getAuditM2Muser(), + await WorkPeriodService.createWorkPeriod( { resourceBookingId: resourceBookingId, startDate: period.startDate, endDate: period.endDate, daysWorked: period.daysWorked, - paymentStatus: 'pending' + paymentStatus: period.daysWorked === 0 ? 'noDays' : 'pending' }) } } @@ -320,7 +322,7 @@ async function _updateWorkPeriods (periods) { */ async function _deleteWorkPeriods (workPeriods) { for (const period of workPeriods) { - await WorkPeriodService.deleteWorkPeriod(helper.getAuditM2Muser(), period.id) + await WorkPeriodService.deleteWorkPeriod(period.id) } } diff --git a/src/eventHandlers/WorkPeriodPaymentEventHandler.js b/src/eventHandlers/WorkPeriodPaymentEventHandler.js new file mode 100644 index 00000000..a3913b15 --- /dev/null +++ b/src/eventHandlers/WorkPeriodPaymentEventHandler.js @@ -0,0 +1,87 @@ +/* + * Handle events for WorkPeriodPayment. + */ + +const _ = require('lodash') +const config = require('config') +const models = require('../models') +const logger = require('../common/logger') +const helper = require('../common/helper') +const WorkPeriod = models.WorkPeriod + +/** + * When a WorkPeriodPayment is updated or created, the workPeriod related to + * that WorkPeriodPayment should be updated also. + * @param {object} payload the event payload + * @returns {undefined} + */ +async function updateWorkPeriod (payload) { + const workPeriodPayment = payload.value + // find related workPeriod to evaluate the changes + const workPeriodModel = await WorkPeriod.findById(workPeriodPayment.workPeriodId, { withPayments: true }) + const workPeriod = workPeriodModel.toJSON() + const data = {} + const paymentStatuses = {} + data.daysPaid = 0 + data.paymentTotal = 0 + _.each(workPeriod.payments, payment => { + paymentStatuses[payment.status] = true + if (_.includes(['scheduled', 'in-progress', 'completed'], payment.status)) { + data.daysPaid += payment.days + data.paymentTotal += payment.amount + } + }) + if (workPeriod.daysWorked === 0) { + data.paymentStatus = 'noDays' + } else if (paymentStatuses.scheduled || paymentStatuses['in-progress']) { + data.paymentStatus = 'in-progress' + } else if (workPeriod.daysWorked === data.daysPaid) { + data.paymentStatus = 'completed' + } else if (paymentStatuses.completed) { + data.paymentStatus = 'partially-completed' + } else if (paymentStatuses.failed) { + data.paymentStatus = 'failed' + } else { + data.paymentStatus = 'pending' + } + if (workPeriod.daysPaid === data.daysPaid && workPeriod.paymentTotal === data.paymentTotal && workPeriod.paymentStatus === data.paymentStatus) { + logger.debug({ + component: 'WorkPeriodPaymentEventHandler', + context: 'updateWorkPeriod', + message: `id: ${workPeriod.id} WorkPeriod has no change - ignored` + }) + return + } + const updated = await workPeriodModel.update(data) + await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, _.omit(updated.toJSON(), 'payments'), { oldValue: workPeriod, key: `resourceBooking.id:${workPeriod.resourceBookingId}` }) + logger.debug({ + component: 'WorkPeriodPaymentEventHandler', + context: 'updateWorkPeriod', + message: `id: ${workPeriod.id} WorkPeriod updated` + }) +} + +/** + * Process work period payment create event. + * + * @param {Object} payload the event payload + * @returns {undefined} + */ +async function processCreate (payload) { + await updateWorkPeriod(payload) +} + +/** + * Process work period payment update event. + * + * @param {Object} payload the event payload + * @returns {undefined} + */ +async function processUpdate (payload) { + await updateWorkPeriod(payload) +} + +module.exports = { + processCreate, + processUpdate +} diff --git a/src/eventHandlers/index.js b/src/eventHandlers/index.js index c88ef929..3f308b18 100644 --- a/src/eventHandlers/index.js +++ b/src/eventHandlers/index.js @@ -9,6 +9,7 @@ const JobCandidateEventHandler = require('./JobCandidateEventHandler') const ResourceBookingEventHandler = require('./ResourceBookingEventHandler') const InterviewEventHandler = require('./InterviewEventHandler') const RoleEventHandler = require('./RoleEventHandler') +const WorkPeriodPaymentEventHandler = require('./WorkPeriodPaymentEventHandler') const logger = require('../common/logger') const TopicOperationMapping = { @@ -18,6 +19,8 @@ const TopicOperationMapping = { [config.TAAS_RESOURCE_BOOKING_CREATE_TOPIC]: ResourceBookingEventHandler.processCreate, [config.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC]: ResourceBookingEventHandler.processUpdate, [config.TAAS_RESOURCE_BOOKING_DELETE_TOPIC]: ResourceBookingEventHandler.processDelete, + [config.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC]: WorkPeriodPaymentEventHandler.processCreate, + [config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC]: WorkPeriodPaymentEventHandler.processUpdate, [config.TAAS_INTERVIEW_REQUEST_TOPIC]: InterviewEventHandler.processRequest, [config.TAAS_ROLE_DELETE_TOPIC]: RoleEventHandler.processDelete } diff --git a/src/models/WorkPeriod.js b/src/models/WorkPeriod.js index 95557368..720e4870 100644 --- a/src/models/WorkPeriod.js +++ b/src/models/WorkPeriod.js @@ -81,13 +81,15 @@ module.exports = (sequelize) => { type: Sequelize.INTEGER, allowNull: false }, - memberRate: { - field: 'member_rate', - type: Sequelize.FLOAT + daysPaid: { + field: 'days_paid', + type: Sequelize.INTEGER, + allowNull: false }, - customerRate: { - field: 'customer_rate', - type: Sequelize.FLOAT + paymentTotal: { + field: 'payment_total', + type: Sequelize.FLOAT, + allowNull: false }, paymentStatus: { field: 'payment_status', diff --git a/src/models/WorkPeriodPayment.js b/src/models/WorkPeriodPayment.js index 8d23487a..bac461d0 100644 --- a/src/models/WorkPeriodPayment.js +++ b/src/models/WorkPeriodPayment.js @@ -48,8 +48,22 @@ module.exports = (sequelize) => { field: 'challenge_id', type: Sequelize.UUID }, + memberRate: { + field: 'member_rate', + type: Sequelize.FLOAT, + allowNull: false + }, + customerRate: { + field: 'customer_rate', + type: Sequelize.FLOAT + }, + days: { + type: Sequelize.INTEGER, + allowNull: false + }, amount: { - type: Sequelize.DOUBLE + type: Sequelize.DOUBLE, + allowNull: false }, status: { type: Sequelize.ENUM(_.values(WorkPeriodPaymentStatus)), @@ -61,7 +75,8 @@ module.exports = (sequelize) => { }, billingAccountId: { field: 'billing_account_id', - type: Sequelize.BIGINT + type: Sequelize.BIGINT, + allowNull: false }, createdBy: { field: 'created_by', diff --git a/src/routes/WorkPeriodPaymentRoutes.js b/src/routes/WorkPeriodPaymentRoutes.js index 3b6f6ba9..7ddd2bc5 100644 --- a/src/routes/WorkPeriodPaymentRoutes.js +++ b/src/routes/WorkPeriodPaymentRoutes.js @@ -33,12 +33,6 @@ module.exports = { auth: 'jwt', scopes: [constants.Scopes.READ_WORK_PERIOD_PAYMENT, constants.Scopes.ALL_WORK_PERIOD_PAYMENT] }, - put: { - controller: 'WorkPeriodPaymentController', - method: 'fullyUpdateWorkPeriodPayment', - auth: 'jwt', - scopes: [constants.Scopes.UPDATE_WORK_PERIOD_PAYMENT, constants.Scopes.ALL_WORK_PERIOD_PAYMENT] - }, patch: { controller: 'WorkPeriodPaymentController', method: 'partiallyUpdateWorkPeriodPayment', diff --git a/src/routes/WorkPeriodRoutes.js b/src/routes/WorkPeriodRoutes.js index 7f84b5b0..659e4d58 100644 --- a/src/routes/WorkPeriodRoutes.js +++ b/src/routes/WorkPeriodRoutes.js @@ -5,12 +5,6 @@ const constants = require('../../app-constants') module.exports = { '/work-periods': { - post: { - controller: 'WorkPeriodController', - method: 'createWorkPeriod', - auth: 'jwt', - scopes: [constants.Scopes.CREATE_WORK_PERIOD, constants.Scopes.ALL_WORK_PERIOD] - }, get: { controller: 'WorkPeriodController', method: 'searchWorkPeriods', @@ -25,23 +19,11 @@ module.exports = { auth: 'jwt', scopes: [constants.Scopes.READ_WORK_PERIOD, constants.Scopes.ALL_WORK_PERIOD] }, - put: { - controller: 'WorkPeriodController', - method: 'fullyUpdateWorkPeriod', - auth: 'jwt', - scopes: [constants.Scopes.UPDATE_WORK_PERIOD, constants.Scopes.ALL_WORK_PERIOD] - }, patch: { controller: 'WorkPeriodController', method: 'partiallyUpdateWorkPeriod', auth: 'jwt', scopes: [constants.Scopes.UPDATE_WORK_PERIOD, constants.Scopes.ALL_WORK_PERIOD] - }, - delete: { - controller: 'WorkPeriodController', - method: 'deleteWorkPeriod', - auth: 'jwt', - scopes: [constants.Scopes.DELETE_WORK_PERIOD, constants.Scopes.ALL_WORK_PERIOD] } } } diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 39529a12..5cd2e705 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -106,8 +106,8 @@ function _checkCriteriaAndGetFields (currentUser, criteria) { // "currentUser.isMachine" to be true is not enough to return "workPeriods.memberRate" // but returning "workPeriod" will be evaluated later if (!canSeeMemberRate) { - result.excludeRB.push('memberRate') - result.excludeWP.push('workPeriods.memberRate') + result.excludeRB.push('paymentTotal') + result.excludeWP.push('workPeriods.paymentTotal') } // if "fields" is not included in cretia, then only ResourceBooking model will be returned // No further evaluation is required as long as the criteria does not include a WorkPeriod filter or a WorkPeriod sorting condition @@ -137,8 +137,8 @@ function _checkCriteriaAndGetFields (currentUser, criteria) { throw new errors.BadRequestError('Can not filter or sort by some field which is not included in fields') } // Check if the current user has no right to see the memberRate and memberRate is included in fields parameter - if (!canSeeMemberRate && _.some(query, q => _.includes(['memberRate', 'workPeriods.memberRate'], q))) { - throw new errors.ForbiddenError('You don\'t have access to view memberRate') + if (!canSeeMemberRate && _.some(query, q => _.includes(['memberRate', 'workPeriods.paymentTotal'], q))) { + throw new errors.ForbiddenError('You don\'t have access to view memberRate and paymentTotal') } // Check if the current user has no right to see the workPeriods and workPeriods is included in fields parameter if (currentUser.isMachine && result.withWorkPeriods && !_checkUserScopesForGetWorkPeriods(currentUser)) { @@ -172,7 +172,7 @@ async function _ensurePaidWorkPeriodsNotDeleted (resourceBookingId, oldValue, ne const paidWorkPeriods = _.filter(workPeriods, workPeriod => { // filter by WP and WPP status return (['completed', 'partially-completed', 'in-progress'].indexOf(workPeriod.paymentStatus) !== -1 || - _.some(workPeriod.payments, payment => ['completed', 'in-progress'].indexOf(payment.status) !== -1)) + _.some(workPeriod.payments, payment => ['completed', 'in-progress', 'shceduled'].indexOf(payment.status) !== -1)) }) if (paidWorkPeriods.length > 0) { throw new errors.BadRequestError(`WorkPeriods with id of ${_.map(paidWorkPeriods, workPeriod => workPeriod.id)} @@ -181,13 +181,13 @@ async function _ensurePaidWorkPeriodsNotDeleted (resourceBookingId, oldValue, ne } // find related workPeriods to evaluate the changes // We don't need to include WPP because WPP's status changes should - // update WP's status. In case of any bug, it's better to check both WP + // update WP's status. In case of any bug or slow processing, it's better to check both WP // and WPP status for now. let workPeriods = await WorkPeriod.findAll({ where: { resourceBookingId: resourceBookingId }, - attributes: ['id', 'paymentStatus', 'startDate', 'endDate'], + attributes: ['id', 'paymentStatus', 'startDate', 'endDate', 'daysPaid'], include: [{ model: WorkPeriodPayment, as: 'payments', @@ -218,6 +218,16 @@ async function _ensurePaidWorkPeriodsNotDeleted (resourceBookingId, oldValue, ne // we can't delete workperiods with paymentStatus 'partially-completed', 'completed' or 'in-progress', // or any of it's WorkPeriodsPayment has status 'completed' or 'in-progress'. _checkForPaidWorkPeriods(workPeriodsToRemove) + // check if this update makes maximum possible daysWorked value less than daysPaid + _.each(newWorkPeriods, newWP => { + const wp = _.find(workPeriods, ['startDate', newWP.startDate]) + if (!wp) { + return + } + if (wp.daysPaid > newWP.daysWorked) { + throw new errors.ConflictError(`Cannot make maximum daysWorked (${newWP.daysWorked}) to the value less than daysPaid (${wp.daysPaid}) for WorkPeriod: ${wp.id}`) + } + }) } /** diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index 80299628..cad84da3 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -16,6 +16,7 @@ const { WorkPeriodPaymentStatus } = require('../../app-constants') const { searchResourceBookings } = require('./ResourceBookingService') const WorkPeriodPayment = models.WorkPeriodPayment +const WorkPeriod = models.WorkPeriod const esClient = helper.getESClient() /** @@ -25,7 +26,7 @@ const esClient = helper.getESClient() * @param {Object} currentUser the user who perform this operation. * @returns {undefined} */ -async function _checkUserPermissionForCRUWorkPeriodPayment (currentUser) { +function _checkUserPermissionForCRUWorkPeriodPayment (currentUser) { if (!currentUser.hasManagePermission && !currentUser.isMachine) { throw new errors.ForbiddenError('You are not allowed to perform this action!') } @@ -43,7 +44,7 @@ async function _createSingleWorkPeriodPayment (workPeriodPayment, createdBy) { // get billingAccountId from corresponding resource booking const correspondingResourceBooking = await helper.ensureResourceBookingById(correspondingWorkPeriod.resourceBookingId) - return _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking(workPeriodPayment, createdBy, correspondingWorkPeriod, correspondingResourceBooking) + return _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking(workPeriodPayment, createdBy, correspondingWorkPeriod.toJSON(), correspondingResourceBooking.toJSON()) } /** @@ -55,47 +56,29 @@ async function _createSingleWorkPeriodPayment (workPeriodPayment, createdBy) { * @returns {Object} the created workPeriodPayment */ async function _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking (workPeriodPayment, createdBy, correspondingWorkPeriod, correspondingResourceBooking) { - if (!correspondingResourceBooking.billingAccountId) { - throw new errors.ConflictError(`id: ${correspondingWorkPeriod.resourceBookingId} "ResourceBooking" Billing account is not assigned to the resource booking`) + if (_.isNil(correspondingResourceBooking.billingAccountId)) { + throw new errors.ConflictError(`id: ${correspondingResourceBooking.id} "ResourceBooking" Billing account is not assigned to the resource booking`) } workPeriodPayment.billingAccountId = correspondingResourceBooking.billingAccountId + if (_.isNil(correspondingResourceBooking.memberRate)) { + throw new errors.ConflictError(`Can't find a member rate in ResourceBooking: ${correspondingResourceBooking.id} to calculate the amount`) + } + workPeriodPayment.memberRate = correspondingResourceBooking.memberRate + const maxPossibleDays = correspondingWorkPeriod.daysWorked - correspondingWorkPeriod.daysPaid + if (workPeriodPayment.days > maxPossibleDays) { + throw new errors.BadRequestError(`Days cannot be more than not paid days which is ${maxPossibleDays}`) + } + if (maxPossibleDays <= 0) { + throw new errors.ConflictError(`There are no days to pay for WorkPeriod: ${correspondingWorkPeriod.id}`) + } + workPeriodPayment.days = _.defaultTo(workPeriodPayment.days, maxPossibleDays) + workPeriodPayment.amount = _.round(workPeriodPayment.memberRate * workPeriodPayment.days / 5, 2) + workPeriodPayment.customerRate = _.defaultTo(correspondingResourceBooking.customerRate, null) workPeriodPayment.id = uuid.v4() workPeriodPayment.status = WorkPeriodPaymentStatus.SCHEDULED workPeriodPayment.createdBy = createdBy - // set workPeriodPayment amount - if (_.isNil(workPeriodPayment.amount)) { - const memberRate = correspondingWorkPeriod.memberRate || correspondingResourceBooking.memberRate - if (_.isNil(memberRate)) { - throw new errors.BadRequestError(`Can't find a member rate in work period: ${workPeriodPayment.workPeriodId} to calculate the amount`) - } - let daysWorked = 0 - if (correspondingWorkPeriod.daysWorked) { - daysWorked = correspondingWorkPeriod.daysWorked - } else { - const matchDW = _.find(helper.extractWorkPeriods(correspondingResourceBooking.startDate, correspondingResourceBooking.endDate), { startDate: correspondingWorkPeriod.startDate }) - if (matchDW) { - daysWorked = matchDW.daysWorked - } - } - if (daysWorked === 0) { - workPeriodPayment.amount = 0 - } else { - workPeriodPayment.amount = _.round(memberRate * daysWorked / 5, 2) - } - } - - let created = null - try { - created = await WorkPeriodPayment.create(workPeriodPayment) - } catch (err) { - if (!_.isUndefined(err.original)) { - throw new errors.BadRequestError(err.original.detail) - } else { - throw err - } - } - + const created = await WorkPeriodPayment.create(workPeriodPayment) await helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC, created.toJSON(), { key: `workPeriodPayment.billingAccountId:${workPeriodPayment.billingAccountId}` }) return created.dataValues } @@ -109,7 +92,7 @@ async function _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking (w */ async function getWorkPeriodPayment (currentUser, id, fromDb = false) { // check user permission - await _checkUserPermissionForCRUWorkPeriodPayment(currentUser) + _checkUserPermissionForCRUWorkPeriodPayment(currentUser) if (!fromDb) { try { const resourceBooking = await esClient.search({ @@ -173,7 +156,7 @@ getWorkPeriodPayment.schema = Joi.object().keys({ */ async function createWorkPeriodPayment (currentUser, workPeriodPayment) { // check permission - await _checkUserPermissionForCRUWorkPeriodPayment(currentUser) + _checkUserPermissionForCRUWorkPeriodPayment(currentUser) const createdBy = await helper.getUserId(currentUser.userId) if (_.isArray(workPeriodPayment)) { @@ -194,7 +177,7 @@ async function createWorkPeriodPayment (currentUser, workPeriodPayment) { const singleCreateWorkPeriodPaymentSchema = Joi.object().keys({ workPeriodId: Joi.string().uuid().required(), - amount: Joi.number().greater(0).allow(null) + days: Joi.number().integer().min(1).max(5) }) createWorkPeriodPayment.schema = Joi.object().keys({ currentUser: Joi.object().required(), @@ -213,27 +196,29 @@ createWorkPeriodPayment.schema = Joi.object().keys({ */ async function updateWorkPeriodPayment (currentUser, id, data) { // check permission - await _checkUserPermissionForCRUWorkPeriodPayment(currentUser) + _checkUserPermissionForCRUWorkPeriodPayment(currentUser) - if (data.workPeriodId) { - // ensure work period exists - await helper.ensureWorkPeriodById(data.workPeriodId) - } const workPeriodPayment = await WorkPeriodPayment.findById(id) const oldValue = workPeriodPayment.toJSON() - - data.updatedBy = await helper.getUserId(currentUser.userId) - let updated = null - try { - updated = await workPeriodPayment.update(data) - } catch (err) { - if (!_.isUndefined(err.original)) { - throw new errors.BadRequestError(err.original.detail) - } else { - throw err + if (oldValue.status === data.status) { + return oldValue + } + if (data.status === 'cancelled' && oldValue.status === 'in-progress') { + throw new errors.BadRequestError('You cannot cancel a WorkPeriodPayment which is in-progress') + } + if (data.status === 'scheduled') { + if (oldValue.status !== 'failed') { + throw new errors.BadRequestError(`You cannot schedule a WorkPeriodPayment which is ${oldValue.status}`) + } + const workPeriod = WorkPeriod.findById(workPeriodPayment.workPeriodId) + // we con't check if paymentStatus is 'completed' + // because paymentStatus can be in-progress when daysWorked = daysPaid + if (workPeriod.daysWorked === workPeriod.daysPaid) { + throw new errors.BadRequestError('There is no available daysWorked to schedule a payment') } } - + data.updatedBy = await helper.getUserId(currentUser.userId) + const updated = await workPeriodPayment.update(data) await helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue, key: `workPeriodPayment.billingAccountId:${updated.billingAccountId}` }) return updated.dataValues } @@ -253,31 +238,8 @@ partiallyUpdateWorkPeriodPayment.schema = Joi.object().keys({ currentUser: Joi.object().required(), id: Joi.string().uuid().required(), data: Joi.object().keys({ - workPeriodId: Joi.string().uuid(), - amount: Joi.number().greater(0).allow(null), - status: Joi.workPeriodPaymentStatus() - }).required() -}).required() - -/** - * Fully update workPeriodPayment by id - * @param {Object} currentUser the user who perform this operation - * @param {String} id the workPeriodPayment id - * @param {Object} data the data to be updated - * @returns {Object} the updated workPeriodPayment - */ -async function fullyUpdateWorkPeriodPayment (currentUser, id, data) { - return updateWorkPeriodPayment(currentUser, id, data) -} - -fullyUpdateWorkPeriodPayment.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.string().uuid().required(), - data: Joi.object().keys({ - workPeriodId: Joi.string().uuid().required(), - amount: Joi.number().greater(0).allow(null), - status: Joi.workPeriodPaymentStatus() - }).required() + status: Joi.workPeriodPaymentUpdateStatus() + }).min(1).required() }).required() /** @@ -289,7 +251,7 @@ fullyUpdateWorkPeriodPayment.schema = Joi.object().keys({ */ async function searchWorkPeriodPayments (currentUser, criteria, options = { returnAll: false }) { // check user permission - await _checkUserPermissionForCRUWorkPeriodPayment(currentUser) + _checkUserPermissionForCRUWorkPeriodPayment(currentUser) if ((typeof criteria.workPeriodIds) === 'string') { criteria.workPeriodIds = criteria.workPeriodIds.trim().split(',').map(workPeriodIdRaw => { @@ -415,12 +377,12 @@ searchWorkPeriodPayments.schema = Joi.object().keys({ */ async function createQueryWorkPeriodPayments (currentUser, criteria) { // check permission - await _checkUserPermissionForCRUWorkPeriodPayment(currentUser) + _checkUserPermissionForCRUWorkPeriodPayment(currentUser) const createdBy = await helper.getUserId(currentUser.userId) const query = criteria.query const fields = _.join(_.uniq(_.concat( - ['id', 'billingAccountId', 'memberRate', 'startDate', 'endDate', 'workPeriods.id', 'workPeriods.resourceBookingId', 'workPeriods.memberRate', 'workPeriods.daysWorked', 'workPeriods.startDate'], + ['id', 'billingAccountId', 'memberRate', 'customerRate', 'workPeriods.id', 'workPeriods.resourceBookingId', 'workPeriods.daysWorked', 'workPeriods.daysPaid'], _.map(_.keys(query), k => k === 'projectIds' ? 'projectId' : k)) ), ',') const searchResult = await searchResourceBookings(currentUser, _.extend({ fields, page: 1 }, query), { returnAll: true }) @@ -456,7 +418,7 @@ createQueryWorkPeriodPayments.schema = Joi.object().keys({ Joi.string(), Joi.array().items(Joi.number().integer()) ), - 'workPeriods.paymentStatus': Joi.paymentStatus(), + 'workPeriods.paymentStatus': Joi.string().valid('pending', 'partially-completed', 'failed'), 'workPeriods.startDate': Joi.date().format('YYYY-MM-DD'), 'workPeriods.endDate': Joi.date().format('YYYY-MM-DD'), 'workPeriods.userHandle': Joi.string() @@ -469,6 +431,5 @@ module.exports = { createWorkPeriodPayment, createQueryWorkPeriodPayments, partiallyUpdateWorkPeriodPayment, - fullyUpdateWorkPeriodPayment, searchWorkPeriodPayments } diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index e94896c8..33802c77 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -97,8 +97,8 @@ function _getWorkPeriodFilteringFields (currentUser) { withPayments: false } if (!currentUser.hasManagePermission && !currentUser.isMachine) { - queryOpt.excludeES.push('workPeriods.memberRate') - queryOpt.excludeDB.push('memberRate') + queryOpt.excludeES.push('workPeriods.paymentTotal') + queryOpt.excludeDB.push('paymentTotal') } if (currentUser.hasManagePermission || _checkUserScopesForGetPayments(currentUser)) { queryOpt.withPayments = true @@ -205,13 +205,10 @@ getWorkPeriod.schema = Joi.object().keys({ /** * Create workPeriod - * @param {Object} currentUser the user who perform this operation * @param {Object} workPeriod the workPeriod to be created * @returns {Object} the created workPeriod */ -async function createWorkPeriod (currentUser, workPeriod) { - // check permission - await _checkUserPermissionForWriteWorkPeriod(currentUser) +async function createWorkPeriod (workPeriod) { // If one of the dates are missing then auto-calculate it _autoCalculateDates(workPeriod) @@ -222,7 +219,7 @@ async function createWorkPeriod (currentUser, workPeriod) { workPeriod.userHandle = user.handle workPeriod.id = uuid.v4() - workPeriod.createdBy = await helper.getUserId(currentUser.userId) + workPeriod.createdBy = config.m2m.M2M_AUDIT_USER_ID let created = null try { @@ -240,14 +237,13 @@ async function createWorkPeriod (currentUser, workPeriod) { } createWorkPeriod.schema = Joi.object().keys({ - currentUser: Joi.object().required(), workPeriod: Joi.object().keys({ resourceBookingId: Joi.string().uuid().required(), startDate: Joi.workPeriodStartDate(), endDate: Joi.workPeriodEndDate(), daysWorked: Joi.number().integer().min(0).max(5).required(), - memberRate: Joi.number().allow(null), - customerRate: Joi.number().allow(null), + daysPaid: Joi.number().default(0).forbidden(), + paymentTotal: Joi.number().default(0).forbidden(), paymentStatus: Joi.paymentStatus().required() }).required() }).required() @@ -265,45 +261,36 @@ async function updateWorkPeriod (currentUser, id, data) { const workPeriod = await WorkPeriod.findById(id) const oldValue = workPeriod.toJSON() - let resourceBooking - // if resourceBookingId is provided then update projectId and userHandle - if (data.resourceBookingId) { - resourceBooking = await helper.ensureResourceBookingById(data.resourceBookingId) // ensure resource booking exists - data.projectId = resourceBooking.projectId - - const user = await helper.ensureUserById(resourceBooking.userId) // ensure user exists - data.userHandle = user.handle - } else { - resourceBooking = await helper.ensureResourceBookingById(oldValue.resourceBookingId) + if (oldValue.daysWorked === data.daysWorked) { + return oldValue } - // If one of the dates are missing then auto-calculate it - _autoCalculateDates(data) - if (data.daysWorked) { - const weeks = helper.extractWorkPeriods(resourceBooking.startDate, resourceBooking.endDate) - if (_.isEmpty(weeks)) { - throw new errors.ConflictError('Resource booking has missing dates') - } - const thisWeek = _.find(weeks, ['startDate', oldValue.startDate]) - if (_.isNil(thisWeek)) { - throw new errors.ConflictError('Work Period dates are not compatible with Resource Booking dates') - } - if (thisWeek.daysWorked < data.daysWorked) { - throw new errors.BadRequestError(`Maximum allowed daysWorked is (${thisWeek.daysWorked})`) - } + if (data.daysWorked < oldValue.daysPaid) { + throw new errors.BadRequestError(`Cannot update daysWorked (${data.daysWorked}) to the value less than daysPaid (${oldValue.daysPaid})`) } - data.updatedBy = await helper.getUserId(currentUser.userId) - let updated = null - try { - updated = await workPeriod.update(data) - } catch (err) { - if (!_.isUndefined(err.original)) { - throw new errors.BadRequestError(err.original.detail) - } else { - throw err - } + const resourceBooking = await helper.ensureResourceBookingById(oldValue.resourceBookingId) + const weeks = helper.extractWorkPeriods(resourceBooking.startDate, resourceBooking.endDate) + if (_.isEmpty(weeks)) { + throw new errors.ConflictError('Resource booking has missing dates') } - - await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue, key: `resourceBooking.id:${data.resourceBookingId}` }) + const thisWeek = _.find(weeks, ['startDate', oldValue.startDate]) + if (_.isNil(thisWeek)) { + throw new errors.ConflictError('Work Period dates are not compatible with Resource Booking dates') + } + if (thisWeek.daysWorked < data.daysWorked) { + throw new errors.BadRequestError(`Maximum allowed daysWorked is (${thisWeek.daysWorked})`) + } + if (data.daysWorked > oldValue.daysWorked && oldValue.paymentStatus === 'completed') { + data.paymentStatus = 'partially-completed' + } else if (data.daysWorked > oldValue.daysWorked && oldValue.paymentStatus === 'noDays') { + data.paymentStatus = 'pending' + } else if (data.daysWorked === oldValue.daysPaid && _.includes(['partially-completed', 'failed'], oldValue.paymentStatus)) { + data.paymentStatus = 'completed' + } else if (data.daysWorked === 0) { + data.paymentStatus = 'noDays' + } + data.updatedBy = await helper.getUserId(currentUser.userId) + const updated = await workPeriod.update(data) + await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue, key: `resourceBooking.id:${updated.resourceBookingId}` }) return updated.dataValues } @@ -322,71 +309,32 @@ partiallyUpdateWorkPeriod.schema = Joi.object().keys({ currentUser: Joi.object().required(), id: Joi.string().uuid().required(), data: Joi.object().keys({ - resourceBookingId: Joi.string().uuid(), - startDate: Joi.workPeriodStartDate(), - endDate: Joi.workPeriodEndDateOptional(), - daysWorked: Joi.number().integer().min(0).max(5), - memberRate: Joi.number().allow(null), - customerRate: Joi.number().allow(null), - paymentStatus: Joi.paymentStatus() - }).required() -}).required() - -/** - * Fully update workPeriod by id - * @param {Object} currentUser the user who perform this operation - * @param {String} id the workPeriod id - * @param {Object} data the data to be updated - * @returns {Object} the updated workPeriod - */ -async function fullyUpdateWorkPeriod (currentUser, id, data) { - return updateWorkPeriod(currentUser, id, data) -} - -fullyUpdateWorkPeriod.schema = Joi.object().keys({ - currentUser: Joi.object().required(), - id: Joi.string().uuid().required(), - data: Joi.object().keys({ - resourceBookingId: Joi.string().uuid().required(), - startDate: Joi.workPeriodStartDate(), - endDate: Joi.workPeriodEndDate(), - daysWorked: Joi.number().integer().min(0).required(), - memberRate: Joi.number().allow(null).default(null), - customerRate: Joi.number().allow(null).default(null), - paymentStatus: Joi.paymentStatus().required() - }).required() + daysWorked: Joi.number().integer().min(0).max(5) + }).required().min(1) }).required() /** * Delete workPeriod by id - * @params {Object} currentUser the user who perform this operation - * @params {String} id the workPeriod id + * @param {String} id the workPeriod id */ -async function deleteWorkPeriod (currentUser, id) { - // check permission - if (!currentUser.hasManagePermission && !currentUser.isMachine) { - throw new errors.ForbiddenError('You are not allowed to perform this action!') - } - +async function deleteWorkPeriod (id) { const workPeriod = await WorkPeriod.findById(id, { withPayments: true }) if (_.includes(['completed', 'partially-completed', 'in-progress'], workPeriod.paymentStatus)) { throw new errors.BadRequestError("Can't delete WorkPeriod with paymentStatus completed partially-completed, or in-progress") } - if (_.some(workPeriod.payments, payment => ['completed', 'in-progress'].indexOf(payment.status) !== -1)) { - throw new errors.BadRequestError("Can't delete WorkPeriod if any associated WorkPeriodsPayment has status completed or in-progress") + if (_.some(workPeriod.payments, payment => ['completed', 'in-progress', 'shceduled'].indexOf(payment.status) !== -1)) { + throw new errors.BadRequestError("Can't delete WorkPeriod if any associated WorkPeriodsPayment has status completed, shceduled or in-progress") } await models.WorkPeriodPayment.destroy({ where: { workPeriodId: id } }) - await Promise.all(workPeriod.payments.map(({ id, billingAccountId }) => helper.postEvent(config.TAAS_WORK_PERIOD_PAYMENT_DELETE_TOPIC, { id }, { key: `workPeriodPayment.billingAccountId:${billingAccountId}` }))) await workPeriod.destroy() await helper.postEvent(config.TAAS_WORK_PERIOD_DELETE_TOPIC, { id }, { key: `resourceBooking.id:${workPeriod.resourceBookingId}` }) } deleteWorkPeriod.schema = Joi.object().keys({ - currentUser: Joi.object().required(), id: Joi.string().uuid().required() }).required() @@ -574,7 +522,6 @@ module.exports = { getWorkPeriod, createWorkPeriod, partiallyUpdateWorkPeriod, - fullyUpdateWorkPeriod, deleteWorkPeriod, searchWorkPeriods } diff --git a/test/unit/ResourceBookingService.test.js b/test/unit/ResourceBookingService.test.js index a66281b4..97335d86 100644 --- a/test/unit/ResourceBookingService.test.js +++ b/test/unit/ResourceBookingService.test.js @@ -50,11 +50,11 @@ describe('resourceBooking service test', () => { expect(stubCreateWorkPeriodService.callCount).to.eq(6) expect(stubUpdateWorkPeriodService.callCount).to.eq(0) expect(stubDeleteWorkPeriodService.callCount).to.eq(0) - expect(stubCreateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0]) - expect(stubCreateWorkPeriodService.getCall(1).args[1]).to.deep.eq(data.workPeriod.request[1]) - expect(stubCreateWorkPeriodService.getCall(2).args[1]).to.deep.eq(data.workPeriod.request[2]) - expect(stubCreateWorkPeriodService.getCall(3).args[1]).to.deep.eq(data.workPeriod.request[3]) - expect(stubCreateWorkPeriodService.getCall(4).args[1]).to.deep.eq(data.workPeriod.request[4]) + expect(stubCreateWorkPeriodService.getCall(0).args[0]).to.deep.eq(data.workPeriod.request[0]) + expect(stubCreateWorkPeriodService.getCall(1).args[0]).to.deep.eq(data.workPeriod.request[1]) + expect(stubCreateWorkPeriodService.getCall(2).args[0]).to.deep.eq(data.workPeriod.request[2]) + expect(stubCreateWorkPeriodService.getCall(3).args[0]).to.deep.eq(data.workPeriod.request[3]) + expect(stubCreateWorkPeriodService.getCall(4).args[0]).to.deep.eq(data.workPeriod.request[4]) }) it('T02:Create resource booking start Sunday end Saturday', async () => { const data = testData.T02 @@ -70,7 +70,7 @@ describe('resourceBooking service test', () => { expect(stubCreateWorkPeriodService.callCount).to.eq(1) expect(stubUpdateWorkPeriodService.callCount).to.eq(0) expect(stubDeleteWorkPeriodService.callCount).to.eq(0) - expect(stubCreateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0]) + expect(stubCreateWorkPeriodService.getCall(0).args[0]).to.deep.eq(data.workPeriod.request[0]) }) it('T03:Create resource booking without startDate', async () => { const data = testData.T03 @@ -173,7 +173,7 @@ describe('resourceBooking service test', () => { expect(stubCreateWorkPeriodService.callCount).to.eq(1) expect(stubUpdateWorkPeriodService.callCount).to.eq(1) expect(stubDeleteWorkPeriodService.callCount).to.eq(0) - expect(stubCreateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0].data) + expect(stubCreateWorkPeriodService.getCall(0).args[0]).to.deep.eq(data.workPeriod.request[0].data) expect(stubUpdateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[1].id) expect(stubUpdateWorkPeriodService.getCall(0).args[2]).to.deep.eq(data.workPeriod.request[1].data) }) @@ -194,11 +194,9 @@ describe('resourceBooking service test', () => { expect(stubPostEvent.calledOnce).to.be.true expect(stubWorkPeriodFindAll.called).to.be.true expect(stubCreateWorkPeriodService.callCount).to.eq(1) - expect(stubUpdateWorkPeriodService.callCount).to.eq(1) + expect(stubUpdateWorkPeriodService.callCount).to.eq(0) expect(stubDeleteWorkPeriodService.callCount).to.eq(0) - expect(stubCreateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0].data) - expect(stubUpdateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[1].id) - expect(stubUpdateWorkPeriodService.getCall(0).args[2]).to.deep.eq(data.workPeriod.request[1].data) + expect(stubCreateWorkPeriodService.getCall(0).args[0]).to.deep.eq(data.workPeriod.request[0].data) }) it('T09:Update resource booking startDate and cause work period to be deleted', async () => { const data = testData.T09 @@ -219,7 +217,7 @@ describe('resourceBooking service test', () => { expect(stubCreateWorkPeriodService.callCount).to.eq(0) expect(stubUpdateWorkPeriodService.callCount).to.eq(1) expect(stubDeleteWorkPeriodService.callCount).to.eq(1) - expect(stubDeleteWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0].id) + expect(stubDeleteWorkPeriodService.getCall(0).args[0]).to.deep.eq(data.workPeriod.request[0].id) expect(stubUpdateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[1].id) expect(stubUpdateWorkPeriodService.getCall(0).args[2]).to.deep.eq(data.workPeriod.request[1].data) }) @@ -242,7 +240,7 @@ describe('resourceBooking service test', () => { expect(stubCreateWorkPeriodService.callCount).to.eq(0) expect(stubUpdateWorkPeriodService.callCount).to.eq(1) expect(stubDeleteWorkPeriodService.callCount).to.eq(1) - expect(stubDeleteWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0].id) + expect(stubDeleteWorkPeriodService.getCall(0).args[0]).to.deep.eq(data.workPeriod.request[0].id) expect(stubUpdateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[1].id) expect(stubUpdateWorkPeriodService.getCall(0).args[2]).to.deep.eq(data.workPeriod.request[1].data) }) @@ -289,10 +287,10 @@ describe('resourceBooking service test', () => { expect(stubCreateWorkPeriodService.callCount).to.eq(1) expect(stubUpdateWorkPeriodService.callCount).to.eq(1) expect(stubDeleteWorkPeriodService.callCount).to.eq(1) - expect(stubDeleteWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0].id) + expect(stubDeleteWorkPeriodService.getCall(0).args[0]).to.deep.eq(data.workPeriod.request[0].id) expect(stubUpdateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[1].id) expect(stubUpdateWorkPeriodService.getCall(0).args[2]).to.deep.eq(data.workPeriod.request[1].data) - expect(stubCreateWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[2].data) + expect(stubCreateWorkPeriodService.getCall(0).args[0]).to.deep.eq(data.workPeriod.request[2].data) }) }) describe('Update resource booking unsuccessfully', () => { @@ -368,8 +366,8 @@ describe('resourceBooking service test', () => { expect(stubCreateWorkPeriodService.callCount).to.eq(0) expect(stubUpdateWorkPeriodService.callCount).to.eq(0) expect(stubDeleteWorkPeriodService.callCount).to.eq(2) - expect(stubDeleteWorkPeriodService.getCall(0).args[1]).to.deep.eq(data.workPeriod.request[0].id) - expect(stubDeleteWorkPeriodService.getCall(1).args[1]).to.deep.eq(data.workPeriod.request[1].id) + expect(stubDeleteWorkPeriodService.getCall(0).args[0]).to.deep.eq(data.workPeriod.request[0].id) + expect(stubDeleteWorkPeriodService.getCall(1).args[0]).to.deep.eq(data.workPeriod.request[1].id) }) }) describe('Delete resource booking unsuccessfully', () => { diff --git a/test/unit/WorkPeriodPaymentService.test.js b/test/unit/WorkPeriodPaymentService.test.js index ecc11186..0b5a8aca 100644 --- a/test/unit/WorkPeriodPaymentService.test.js +++ b/test/unit/WorkPeriodPaymentService.test.js @@ -1,6 +1,4 @@ /* eslint-disable no-unused-expressions */ - -// const _ = require('lodash') const expect = require('chai').expect const sinon = require('sinon') const models = require('../../src/models') @@ -8,7 +6,6 @@ const service = require('../../src/services/WorkPeriodPaymentService') const commonData = require('./common/CommonData') const testData = require('./common/WorkPeriodPaymentData') const helper = require('../../src/common/helper') -// const esClient = helper.getESClient() const busApiClient = helper.getBusApiClient() describe('workPeriod service test', () => { beforeEach(() => { @@ -19,7 +16,7 @@ describe('workPeriod service test', () => { sinon.restore() }) - describe('create work period test', () => { + describe('create work period payment test', () => { let stubGetUserId let stubEnsureWorkPeriodById let stubEnsureResourceBookingById @@ -33,22 +30,21 @@ describe('workPeriod service test', () => { }) it('create work period success', async () => { + const stubWorkPeriodFindById = sinon.stub(models.WorkPeriod, 'findOne').callsFake(async () => testData.workPeriodPayment01.workPeriodWithPayments) + const stubUpdateWorkPeriod = sinon.stub(testData.workPeriodPayment01.workPeriodWithPayments, 'update').callsFake(async () => testData.workPeriodPayment01.workPeriodUpdateResponse) const response = await service.createWorkPeriodPayment(commonData.currentUser, testData.workPeriodPayment01.request) expect(stubGetUserId.calledOnce).to.be.true expect(stubEnsureWorkPeriodById.calledOnce).to.be.true expect(stubEnsureResourceBookingById.calledOnce).to.be.true expect(stubCreateWorkPeriodPayment.calledOnce).to.be.true + expect(stubWorkPeriodFindById.calledOnce).to.be.true + expect(stubUpdateWorkPeriod.calledOnce).to.be.true expect(response).to.eql(testData.workPeriodPayment01.response.dataValues) - }) - - it('create work period success - billingAccountId is set', async () => { - await service.createWorkPeriodPayment(commonData.currentUser, testData.workPeriodPayment01.request) - expect(stubCreateWorkPeriodPayment.calledOnce).to.be.true + expect(stubUpdateWorkPeriod.getCall(0).args[0]).to.deep.eq(testData.workPeriodPayment01.workPeriodUpdateRequest) expect(stubCreateWorkPeriodPayment.args[0][0]).to.include({ billingAccountId: testData.workPeriodPayment01.ensureResourceBookingByIdResponse.billingAccountId }) }) - it('fail to create work period if corresponding resource booking does not have bill account', async () => { stubEnsureResourceBookingById.restore() sinon.stub(helper, 'ensureResourceBookingById').callsFake(async () => testData.workPeriodPayment01.ensureResourceBookingByIdResponse02) diff --git a/test/unit/common/ResourceBookingData.js b/test/unit/common/ResourceBookingData.js index 347bd8c5..de98f771 100644 --- a/test/unit/common/ResourceBookingData.js +++ b/test/unit/common/ResourceBookingData.js @@ -35,7 +35,7 @@ const T01 = { startDate: '2021-03-28', endDate: '2021-04-03', daysWorked: 0, - paymentStatus: 'pending' + paymentStatus: 'noDays' }, { resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', @@ -70,7 +70,7 @@ const T01 = { startDate: '2021-05-02', endDate: '2021-05-08', daysWorked: 0, - paymentStatus: 'pending' + paymentStatus: 'noDays' }] } } @@ -250,8 +250,8 @@ const T06 = { startDate: '2021-04-11', endDate: '2021-04-17', daysWorked: 5, - memberRate: null, - customerRate: null, + daysPaid: 0, + paymentTotal: 0, paymentStatus: 'pending', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -322,8 +322,8 @@ const T07 = { startDate: '2021-04-11', endDate: '2021-04-17', daysWorked: 5, - memberRate: null, - customerRate: null, + daysPaid: 0, + paymentTotal: 0, paymentStatus: 'pending', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -339,7 +339,7 @@ const T07 = { startDate: '2021-04-04', endDate: '2021-04-10', daysWorked: 0, - paymentStatus: 'pending' + paymentStatus: 'noDays' } }, { @@ -405,8 +405,8 @@ const T08 = { startDate: '2021-04-11', endDate: '2021-04-17', daysWorked: 5, - memberRate: null, - customerRate: null, + daysPaid: 0, + paymentTotal: 0, paymentStatus: 'pending', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -422,13 +422,7 @@ const T08 = { startDate: '2021-04-18', endDate: '2021-04-24', daysWorked: 0, - paymentStatus: 'pending' - } - }, - { - id: '10faf505-d0e3-4d13-a817-7f1319625e91', - data: { - daysWorked: 5 + paymentStatus: 'noDays' } } ] @@ -488,9 +482,9 @@ const T09 = { startDate: '2021-04-04', endDate: '2021-04-10', daysWorked: 0, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', + daysPaid: 0, + paymentTotal: 0, + paymentStatus: 'noDays', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, createdAt: '2021-04-10T22:25:08.289Z', @@ -506,8 +500,8 @@ const T09 = { startDate: '2021-04-11', endDate: '2021-04-17', daysWorked: 5, - memberRate: null, - customerRate: null, + daysPaid: 0, + paymentTotal: 0, paymentStatus: 'pending', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -583,9 +577,9 @@ const T10 = { startDate: '2021-04-04', endDate: '2021-04-10', daysWorked: 0, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', + daysPaid: 0, + paymentTotal: 0, + paymentStatus: 'noDays', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, createdAt: '2021-04-10T22:25:08.289Z', @@ -601,8 +595,8 @@ const T10 = { startDate: '2021-04-11', endDate: '2021-04-17', daysWorked: 5, - memberRate: null, - customerRate: null, + daysPaid: 0, + paymentTotal: 0, paymentStatus: 'pending', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -678,9 +672,9 @@ const T11 = { startDate: '2021-04-04', endDate: '2021-04-10', daysWorked: 0, - memberRate: null, - customerRate: null, - paymentStatus: 'pending', + daysPaid: 0, + paymentTotal: 0, + paymentStatus: 'noDays', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, createdAt: '2021-04-10T22:25:08.289Z', @@ -696,8 +690,8 @@ const T11 = { startDate: '2021-04-11', endDate: '2021-04-17', daysWorked: 3, - memberRate: null, - customerRate: null, + daysPaid: 0, + paymentTotal: 0, paymentStatus: 'pending', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -772,8 +766,8 @@ const T12 = { startDate: '2021-04-04', endDate: '2021-04-10', daysWorked: 4, - memberRate: null, - customerRate: null, + daysPaid: 0, + paymentTotal: 0, paymentStatus: 'pending', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -790,8 +784,8 @@ const T12 = { startDate: '2021-04-11', endDate: '2021-04-17', daysWorked: 4, - memberRate: null, - customerRate: null, + daysPaid: 1, + paymentTotal: 2.65, paymentStatus: 'partially-completed', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -861,8 +855,8 @@ const T13 = { startDate: '2021-04-04', endDate: '2021-04-10', daysWorked: 4, - memberRate: null, - customerRate: null, + daysPaid: 0, + paymentTotal: 0, paymentStatus: 'pending', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -879,8 +873,8 @@ const T13 = { startDate: '2021-04-11', endDate: '2021-04-17', daysWorked: 4, - memberRate: null, - customerRate: null, + daysPaid: 4, + paymentTotal: 10.59, paymentStatus: 'completed', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -931,8 +925,8 @@ const T14 = { startDate: '2021-04-04', endDate: '2021-04-10', daysWorked: 4, - memberRate: null, - customerRate: null, + daysPaid: 0, + paymentTotal: 0, paymentStatus: 'pending', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -949,8 +943,8 @@ const T14 = { startDate: '2021-04-11', endDate: '2021-04-17', daysWorked: 4, - memberRate: null, - customerRate: null, + daysPaid: 4, + paymentTotal: 10.59, paymentStatus: 'completed', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -992,8 +986,8 @@ const T15 = { startDate: '2021-04-04', endDate: '2021-04-10', daysWorked: 4, - memberRate: null, - customerRate: null, + daysPaid: 0, + paymentTotal: 0, paymentStatus: 'pending', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -1010,8 +1004,8 @@ const T15 = { startDate: '2021-04-11', endDate: '2021-04-17', daysWorked: 4, - memberRate: null, - customerRate: null, + daysPaid: 0, + paymentTotal: 0, paymentStatus: 'pending', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -1067,8 +1061,8 @@ const T16 = { startDate: '2021-04-04', endDate: '2021-04-10', daysWorked: 4, - memberRate: null, - customerRate: null, + daysPaid: 0, + paymentTotal: 0, paymentStatus: 'pending', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -1085,8 +1079,8 @@ const T16 = { startDate: '2021-04-11', endDate: '2021-04-17', daysWorked: 4, - memberRate: null, - customerRate: null, + daysPaid: 4, + paymentTotal: 10.59, paymentStatus: 'completed', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, @@ -1161,7 +1155,7 @@ const T20 = { }, error: { httpStatus: 403, - message: 'You don\'t have access to view memberRate' + message: 'You don\'t have access to view memberRate and paymentTotal' } } const T21 = { @@ -1404,7 +1398,7 @@ const T27 = { }, error: { httpStatus: 403, - message: 'You don\'t have access to view memberRate' + message: 'You don\'t have access to view memberRate and paymentTotal' } } const T28 = { diff --git a/test/unit/common/WorkPeriodPaymentData.js b/test/unit/common/WorkPeriodPaymentData.js index 6e321b62..6c54e525 100644 --- a/test/unit/common/WorkPeriodPaymentData.js +++ b/test/unit/common/WorkPeriodPaymentData.js @@ -1,13 +1,15 @@ const workPeriodPayment01 = { request: { - workPeriodId: '467b4df7-ced4-41b9-9710-b83808cddaf4', - amount: 600 + workPeriodId: '467b4df7-ced4-41b9-9710-b83808cddaf4' }, response: { dataValues: { workPeriodId: '467b4df7-ced4-41b9-9710-b83808cddaf4', amount: 600, status: 'scheduled', + days: 3, + memberRate: 13.23, + customerRate: 13, id: '01971e6f-0f09-4a2a-bc2e-2adac0f00622', challengeId: '00000000-0000-0000-0000-000000000000', createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', @@ -18,23 +20,150 @@ const workPeriodPayment01 = { }, getUserIdResponse: '79a39efd-91af-494a-b0f6-62310495effd', ensureWorkPeriodByIdResponse: { - projectId: 111, userHandle: 'pshah_manager', - endDate: '2021-03-13' + updatedBy: null, + endDate: '2020-10-31', + daysPaid: 2, + resourceBookingId: '8694a939-45fe-482e-bee2-3b530acf4139', + daysWorked: 5, + createdAt: '2021-06-13T18:21:52.564Z', + createdBy: '00000000-0000-0000-0000-000000000000', + paymentTotal: 5.29, + id: '467b4df7-ced4-41b9-9710-b83808cddaf4', + projectId: 17234, + startDate: '2020-10-25', + paymentStatus: 'partially-completed', + updatedAt: '2021-06-13T18:25:08.492Z' + }, + workPeriodWithPayments: { + userHandle: 'pshah_manager', + updatedBy: null, + endDate: '2020-10-31', + daysPaid: 5, + resourceBookingId: '8694a939-45fe-482e-bee2-3b530acf4139', + daysWorked: 5, + createdAt: '2021-06-13T18:21:52.564Z', + createdBy: '00000000-0000-0000-0000-000000000000', + paymentTotal: 5.29, + id: '467b4df7-ced4-41b9-9710-b83808cddaf4', + projectId: 17234, + startDate: '2020-10-25', + paymentStatus: 'in-progress', + updatedAt: '2021-06-13T18:25:08.492Z', + payments: [ + { + amount: 5.29, + updatedBy: null, + billingAccountId: 80000071, + workPeriodId: '467b4df7-ced4-41b9-9710-b83808cddaf4', + createdAt: '2021-06-13T18:22:10.258Z', + challengeId: '00000000-0000-0000-0000-000000000000', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + days: 2, + statusDetails: null, + id: '2a30b5a1-3558-4795-b516-d03cb098fc0f', + status: 'completed', + updatedAt: '2021-06-13T18:25:08.445Z' + }, + { + amount: 7.31, + updatedBy: null, + billingAccountId: 80000071, + workPeriodId: '467b4df7-ced4-41b9-9710-b83808cddaf4', + createdAt: '2021-06-13T18:22:10.258Z', + challengeId: '00000000-0000-0000-0000-000000000000', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + days: 3, + statusDetails: null, + id: '2a30b5a1-3558-4795-b516-d03cb098fc0f', + status: 'scheduled', + updatedAt: '2021-06-13T18:25:08.445Z' + } + ] }, ensureResourceBookingByIdResponse: { - billingAccountId: 68800079 + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: 80000071, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '13c1fcd2-7bbb-4623-8643-ef025dac4c88', + rateType: 'hourly', + createdAt: '2021-06-13T18:21:48.474Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: '8694a939-45fe-482e-bee2-3b530acf4139', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-06-13T18:21:48.474Z' + }, + workPeriodUpdateResponse: { + userHandle: 'pshah_manager', + updatedBy: null, + endDate: '2020-10-31', + daysPaid: 5, + resourceBookingId: '8694a939-45fe-482e-bee2-3b530acf4139', + daysWorked: 5, + createdAt: '2021-06-13T18:21:52.564Z', + createdBy: '00000000-0000-0000-0000-000000000000', + paymentTotal: 12.6, + id: '467b4df7-ced4-41b9-9710-b83808cddaf4', + projectId: 17234, + startDate: '2020-10-25', + paymentStatus: 'in-progress', + updatedAt: '2021-06-13T18:25:08.492Z' + }, + workPeriodUpdateRequest: { + daysPaid: 5, + paymentTotal: 12.6, + paymentStatus: 'in-progress' + }, + ensureResourceBookingByIdResponse02: { + updatedBy: null, + endDate: '2020-10-27', + billingAccountId: null, + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + jobId: '13c1fcd2-7bbb-4623-8643-ef025dac4c88', + rateType: 'hourly', + createdAt: '2021-06-13T18:21:48.474Z', + memberRate: 13.23, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 13, + id: '8694a939-45fe-482e-bee2-3b530acf4139', + projectId: 17234, + startDate: '2020-09-27', + status: 'placed', + updatedAt: '2021-06-13T18:21:48.474Z' }, - ensureResourceBookingByIdResponse02: {}, createPaymentResponse: { id: 'c65f0cbf-b197-423d-91cc-db6e3bad9075' } } workPeriodPayment01.response.toJSON = function () { - return workPeriodPayment01.response + return workPeriodPayment01.response.dataValues +} +workPeriodPayment01.ensureWorkPeriodByIdResponse.toJSON = function () { + return workPeriodPayment01.ensureWorkPeriodByIdResponse +} +workPeriodPayment01.ensureResourceBookingByIdResponse.toJSON = function () { + return workPeriodPayment01.ensureResourceBookingByIdResponse +} +workPeriodPayment01.workPeriodWithPayments.toJSON = function () { + return workPeriodPayment01.workPeriodWithPayments +} +workPeriodPayment01.workPeriodWithPayments.update = function () {} +workPeriodPayment01.workPeriodUpdateResponse.toJSON = function () { + return workPeriodPayment01.workPeriodUpdateResponse +} +workPeriodPayment01.ensureResourceBookingByIdResponse02.toJSON = function () { + return workPeriodPayment01.ensureResourceBookingByIdResponse02 } - module.exports = { workPeriodPayment01 } From 02204c6fba06c3d94c06b03ca66c4c916cd9106d Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Mon, 14 Jun 2021 13:43:18 +0300 Subject: [PATCH 71/86] fix: allow multiple payment status in query payment --- src/services/WorkPeriodPaymentService.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index 38582f41..617a04d2 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -457,7 +457,10 @@ createQueryWorkPeriodPayments.schema = Joi.object().keys({ Joi.string(), Joi.array().items(Joi.number().integer()) ), - 'workPeriods.paymentStatus': Joi.paymentStatus(), + 'workPeriods.paymentStatus': Joi.alternatives( + Joi.string(), + Joi.array().items(Joi.paymentStatus()) + ), 'workPeriods.startDate': Joi.date().format('YYYY-MM-DD'), 'workPeriods.endDate': Joi.date().format('YYYY-MM-DD'), 'workPeriods.userHandle': Joi.string() From 49c63951c7e611bcca3f83344e7b23a013a30afd Mon Sep 17 00:00:00 2001 From: Atif Date: Mon, 14 Jun 2021 16:54:03 +0530 Subject: [PATCH 72/86] Revert "Feature/role jd parser2" --- config/default.js | 2 - ...coder-bookings-api.postman_collection.json | 255 ++------ docs/stopWords.json | 574 ------------------ docs/swagger.yaml | 9 +- src/services/TeamService.js | 130 +--- 5 files changed, 71 insertions(+), 899 deletions(-) delete mode 100644 docs/stopWords.json diff --git a/config/default.js b/config/default.js index 726c40fa..d90ae6c0 100644 --- a/config/default.js +++ b/config/default.js @@ -176,8 +176,6 @@ module.exports = { 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'], - // Topcoder skills cache time in minutes - TOPCODER_SKILLS_CACHE_TIME: process.env.TOPCODER_SKILLS_CACHE_TIME || 60, // payment scheduler config PAYMENT_PROCESSING: { // switch off actual API calls in Payment Scheduler diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index b37011a5..479ddceb 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "d413d21d-272f-454f-b26a-0d7e3bf926d9", + "_postman_id": "18310e1b-429d-49db-8555-f4a54404271f", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -21165,221 +21165,6 @@ } ] }, - { - "name": "Get Skills by Job Description", - "item": [ - { - "name": "get skills successfully", - "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}}" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \"description\": \"Description A global leading healthcare company is seeking a strong Databricks Engineer to join their development team as they build their new Databricks workspace. Development efforts will contribute to the migration of data from Hadoop to Databricks to prepare data for visualization. Candidate must be well-versed in Databricks components and best practices, be an excellent problem solver and be comfortable working in a fast-moving, rapidly changing, and dynamic environment via Agile, SCRUM, and DevOps. PREFERRED QUALIFICATIONS: 2+ years of Azure Data Stack experience: Azure Data Services using ADF, ADLS, Databricks with PySpark, Azure DevOps & Azure Key Vault. Strong knowledge of various data warehousing methodologies and data modeling concepts. Hands-on experience using Azure, Azure data lake, Azure functions & Databricks Minimum 2-3+ years of Python experience (PySpark) Design & Develop Azure native solutions for Data Platform Minimum 3+ years of experience using Big Data ecosystem (Cloudera/Hortonworks) using Oozie, Hive, Impala, and Spark Expert in SQL and performance tuning\" }", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", - "host": [ - "{{URL}}" - ], - "path": [ - "taas-teams", - "getSkillsByJobDescription" - ] - } - }, - "response": [] - }, - { - "name": "get skills by 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", - "type": "text", - "value": "Bearer invalid_token" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \"description\": \"Description A global leading healthcare company is seeking a strong Databricks Engineer to join their development team as they build their new Databricks workspace. Development efforts will contribute to the migration of data from Hadoop to Databricks to prepare data for visualization. Candidate must be well-versed in Databricks components and best practices, be an excellent problem solver and be comfortable working in a fast-moving, rapidly changing, and dynamic environment via Agile, SCRUM, and DevOps. PREFERRED QUALIFICATIONS: 2+ years of Azure Data Stack experience: Azure Data Services using ADF, ADLS, Databricks with PySpark, Azure DevOps & Azure Key Vault. Strong knowledge of various data warehousing methodologies and data modeling concepts. Hands-on experience using Azure, Azure data lake, Azure functions & Databricks Minimum 2-3+ years of Python experience (PySpark) Design & Develop Azure native solutions for Data Platform Minimum 3+ years of experience using Big Data ecosystem (Cloudera/Hortonworks) using Oozie, Hive, Impala, and Spark Expert in SQL and performance tuning\" }", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", - "host": [ - "{{URL}}" - ], - "path": [ - "taas-teams", - "getSkillsByJobDescription" - ] - } - }, - "response": [] - }, - { - "name": "get skills by invalid 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.description\\\" is not allowed to be empty\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_administrator}}" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \"description\": \"\" }", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", - "host": [ - "{{URL}}" - ], - "path": [ - "taas-teams", - "getSkillsByJobDescription" - ] - } - }, - "response": [] - }, - { - "name": "get skills by 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.description\\\" is required\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_administrator}}" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", - "host": [ - "{{URL}}" - ], - "path": [ - "taas-teams", - "getSkillsByJobDescription" - ] - } - }, - "response": [] - } - ] - }, { "name": "GET /taas-teams", "request": { @@ -21863,6 +21648,44 @@ }, "response": [] }, + { + "name": "POST /taas-teams/getSkillsByJobDescription", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"description\": \"nodejs react c++ hello\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "getSkillsByJobDescription" + ] + } + }, + "response": [] + }, { "name": "GET /taas-teams/:id/members", "request": { diff --git a/docs/stopWords.json b/docs/stopWords.json deleted file mode 100644 index dded6681..00000000 --- a/docs/stopWords.json +++ /dev/null @@ -1,574 +0,0 @@ -[ - "dr", - "dra", - "mr", - "ms", - "a", - "a's", - "able", - "about", - "above", - "according", - "accordingly", - "across", - "actually", - "after", - "afterwards", - "again", - "against", - "ain't", - "all", - "allow", - "allows", - "almost", - "alone", - "along", - "already", - "also", - "although", - "always", - "am", - "among", - "amongst", - "an", - "and", - "another", - "any", - "anybody", - "anyhow", - "anyone", - "anything", - "anyway", - "anyways", - "anywhere", - "apart", - "appear", - "appreciate", - "appropriate", - "are", - "aren't", - "around", - "as", - "aside", - "ask", - "asking", - "associated", - "at", - "available", - "away", - "awfully", - "b", - "be", - "became", - "because", - "become", - "becomes", - "becoming", - "been", - "before", - "beforehand", - "behind", - "being", - "believe", - "below", - "beside", - "besides", - "best", - "better", - "between", - "beyond", - "both", - "brief", - "but", - "by", - "c'mon", - "c's", - "came", - "can", - "can't", - "cannot", - "cant", - "cause", - "causes", - "certain", - "certainly", - "changes", - "clearly", - "co", - "come", - "comes", - "concerning", - "consequently", - "consider", - "considering", - "contain", - "containing", - "contains", - "corresponding", - "could", - "couldn't", - "course", - "currently", - "d", - "definitely", - "described", - "despite", - "did", - "didn't", - "different", - "do", - "does", - "doesn't", - "doing", - "don't", - "done", - "down", - "downwards", - "during", - "e", - "each", - "edu", - "eg", - "eight", - "either", - "else", - "elsewhere", - "enough", - "entirely", - "especially", - "et", - "etc", - "even", - "ever", - "every", - "everybody", - "everyone", - "everything", - "everywhere", - "ex", - "exactly", - "example", - "except", - "f", - "far", - "few", - "fifth", - "first", - "five", - "followed", - "following", - "follows", - "for", - "former", - "formerly", - "forth", - "four", - "from", - "further", - "furthermore", - "g", - "get", - "gets", - "getting", - "given", - "gives", - "goes", - "going", - "gone", - "got", - "gotten", - "greetings", - "h", - "had", - "hadn't", - "happens", - "hardly", - "has", - "hasn't", - "have", - "haven't", - "having", - "he", - "he's", - "hello", - "help", - "hence", - "her", - "here", - "here's", - "hereafter", - "hereby", - "herein", - "hereupon", - "hers", - "herself", - "hi", - "him", - "himself", - "his", - "hither", - "hopefully", - "how", - "howbeit", - "however", - "i", - "i'd", - "i'll", - "i'm", - "i've", - "ie", - "if", - "ignored", - "immediate", - "in", - "inasmuch", - "inc", - "indeed", - "indicate", - "indicated", - "indicates", - "inner", - "insofar", - "instead", - "into", - "inward", - "is", - "isn't", - "it", - "it'd", - "it'll", - "it's", - "its", - "itself", - "j", - "just", - "k", - "keep", - "keeps", - "kept", - "know", - "knows", - "known", - "l", - "last", - "lately", - "later", - "latter", - "latterly", - "least", - "lest", - "let", - "let's", - "like", - "liked", - "likely", - "little", - "look", - "looking", - "looks", - "ltd", - "m", - "mainly", - "many", - "may", - "maybe", - "me", - "mean", - "meanwhile", - "merely", - "might", - "more", - "moreover", - "most", - "mostly", - "much", - "must", - "my", - "myself", - "n", - "name", - "namely", - "nd", - "near", - "nearly", - "necessary", - "need", - "needs", - "neither", - "never", - "nevertheless", - "new", - "next", - "nine", - "no", - "nobody", - "non", - "none", - "noone", - "nor", - "normally", - "not", - "nothing", - "novel", - "now", - "nowhere", - "o", - "obviously", - "of", - "off", - "often", - "oh", - "ok", - "okay", - "old", - "on", - "once", - "one", - "ones", - "only", - "onto", - "or", - "other", - "others", - "otherwise", - "ought", - "our", - "ours", - "ourselves", - "out", - "outside", - "over", - "overall", - "own", - "p", - "particular", - "particularly", - "per", - "perhaps", - "placed", - "please", - "plus", - "point", - "possible", - "presumably", - "probably", - "provides", - "q", - "que", - "quite", - "qv", - "rather", - "rd", - "re", - "really", - "reasonably", - "regarding", - "regardless", - "regards", - "relatively", - "respectively", - "right", - "s", - "said", - "same", - "saw", - "say", - "saying", - "says", - "second", - "secondly", - "see", - "seeing", - "seem", - "seemed", - "seeming", - "seems", - "seen", - "self", - "selves", - "sensible", - "sent", - "serious", - "seriously", - "seven", - "several", - "shall", - "she", - "should", - "shouldn't", - "since", - "six", - "so", - "some", - "somebody", - "somehow", - "someone", - "something", - "sometime", - "sometimes", - "somewhat", - "somewhere", - "soon", - "sorry", - "specified", - "specify", - "specifying", - "still", - "strong", - "sub", - "such", - "sup", - "sure", - "t", - "t's", - "take", - "taken", - "tell", - "tends", - "th", - "than", - "thank", - "thanks", - "thanx", - "that", - "that's", - "thats", - "the", - "their", - "theirs", - "them", - "themselves", - "then", - "thence", - "there", - "there's", - "thereafter", - "thereby", - "therefore", - "therein", - "theres", - "thereupon", - "these", - "they", - "they'd", - "they'll", - "they're", - "they've", - "think", - "third", - "this", - "thorough", - "thoroughly", - "those", - "though", - "three", - "through", - "throughout", - "thru", - "thus", - "to", - "together", - "too", - "took", - "toward", - "towards", - "tried", - "tries", - "truly", - "try", - "trying", - "twice", - "two", - "u", - "un", - "under", - "unfortunately", - "unless", - "unlikely", - "until", - "unto", - "up", - "upon", - "us", - "use", - "used", - "useful", - "uses", - "using", - "usually", - "uucp", - "v", - "value", - "various", - "very", - "via", - "viz", - "vs", - "w", - "want", - "wants", - "was", - "wasn't", - "way", - "we", - "we'd", - "we'll", - "we're", - "we've", - "welcome", - "well", - "went", - "were", - "weren't", - "what", - "what's", - "whatever", - "when", - "whence", - "whenever", - "where", - "where's", - "whereafter", - "whereas", - "whereby", - "wherein", - "whereupon", - "wherever", - "whether", - "which", - "while", - "whither", - "who", - "who's", - "whoever", - "whole", - "whom", - "whose", - "why", - "will", - "willing", - "wish", - "with", - "within", - "without", - "won't", - "wonder", - "would", - "would", - "wouldn't", - "x", - "y", - "yes", - "yet", - "you", - "you'd", - "you'll", - "you're", - "you've", - "your", - "yours", - "yourself", - "yourselves", - "z", - "zero" -] \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e6cb5988..cab269c9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3279,6 +3279,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "500": description: Internal Server Error content: @@ -3759,13 +3765,10 @@ components: properties: tag: type: string - example: "Java" type: type: string - example: "taas_skill" source: type: string - example: "taas-jd-parser" Job: required: diff --git a/src/services/TeamService.js b/src/services/TeamService.js index af006603..c32b2776 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -15,10 +15,8 @@ const ResourceBookingService = require('./ResourceBookingService') const HttpStatus = require('http-status-codes') const { Op } = require('sequelize') const models = require('../models') -const stopWords = require('../../docs/stopWords.json') const Role = models.Role const RoleSearchRequest = models.RoleSearchRequest -const topcoderSkills = {} const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { return { @@ -62,73 +60,6 @@ async function _getJobsByProjectIds (currentUser, projectIds) { return result } -/** - * Gets topcoder skills and stores their name and compiled - * regex patters according to Levenshtein distance <=1 - */ -async function _reloadCachedTopcoderSkills () { - // do not reload if cache time is not expired - if (!_.isUndefined(topcoderSkills.time)) { - const cacheTime = config.TOPCODER_SKILLS_CACHE_TIME * 60 * 1000 - if (new Date().getTime() - topcoderSkills.time < cacheTime) { - return - } - } - // collect all skills - const skills = await helper.getAllTopcoderSkills() - // set the last cached time - topcoderSkills.time = new Date().getTime() - topcoderSkills.skills = [] - // store skill names and compiled regex paterns - _.each(skills, skill => { - topcoderSkills.skills.push({ - name: skill.name, - pattern: _compileRegexPatternForSkillName(skill.name) - }) - }) -} - -/** - * Prepares the regex pattern for the given word - * according to Levenshtein distance of 1 (insertions, deletions or substitutions) - * @param {String} skillName the name of the skill - * @returns {RegExp} the compiled regex pattern - */ -function _compileRegexPatternForSkillName (skillName) { - // split the name into its chars - let chars = _.split(skillName, '') - // escape characters reserved to regex - chars = _.map(chars, _.escapeRegExp) - // Its not a good idea to apply tolerance according to - // Levenshtein distance for the words have less than 3 letters - // We expect the skill names have 1 or 2 letters to take place - // in job description as how they are exactly spelled - if (chars.length < 3) { - return new RegExp(`^(?:${_.join(chars, '')})$`, 'i') - } - - const res = [] - // include the skill name itself - res.push(_.join(chars, '')) - // include every transposition combination - // E.g. java => ajva, jvaa, jaav - for (let i = 0; i < chars.length - 1; i++) { - res.push(_.join(_.slice(chars, 0, i), '') + chars[i + 1] + chars[i] + _.join(_.slice(chars, i + 2), '')) - } - // include every insertion combination - // E.g. java => .java, j.ava, ja.va, jav.a, java. - for (let i = 0; i <= chars.length; i++) { - res.push(_.join(_.slice(chars, 0, i), '') + '.' + _.join(_.slice(chars, i), '')) - } - // include every deletion/substitution combination - // E.g. java => .?ava, j.?va, ja.?a, jav.? - for (let i = 0; i < chars.length; i++) { - res.push(_.join(_.slice(chars, 0, i), '') + '.?' + _.join(_.slice(chars, i + 1), '')) - } - // return the regex pattern - return new RegExp(`^(?:${_.join(res, '|')})$`, 'i') -} - /** * List teams * @param {Object} currentUser the user who perform this operation @@ -764,7 +695,9 @@ async function roleSearchRequest (currentUser, data) { } else { // if only job description is provided, collect skill names from description const tags = await getSkillsByJobDescription(currentUser, { description: data.jobDescription }) - const skills = _.map(tags, 'tag') + // 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) } @@ -830,40 +763,7 @@ getRoleBySkills.schema = Joi.object() * @returns {Object} the result */ async function getSkillsByJobDescription (currentUser, data) { - // load topcoder skills if needed. Using cached skills helps to avoid - // unnecessary api calls which is extremely time comsuming. - await _reloadCachedTopcoderSkills() - // replace markdown tags with spaces - let description = _.replace(data.description, /[`|^[\]{}~/,:-]|#{2,}|
/gi, ' ') - // replace all whitespace characters with single space - description = _.replace(description, /\s\s+/g, ' ') - // extract words from description - let words = _.split(description, ' ') - // remove stopwords from description - words = _.filter(words, word => stopWords.indexOf(word.toLowerCase()) === -1) - let foundSkills = [] - const result = [] - // try to match each word with skill names - // using pre-compiled regex pattern - _.each(words, word => { - _.each(topcoderSkills.skills, skill => { - // do not stop searching after a match in order to detect more lookalikes - if (skill.pattern.test(word)) { - foundSkills.push(skill.name) - } - }) - }) - foundSkills = _.uniq(foundSkills) - // apply desired template - _.each(foundSkills, skill => { - result.push({ - tag: skill, - type: 'taas_skill', - source: 'taas-jd-parser' - }) - }) - - return result + return helper.getTags(data.description) } getSkillsByJobDescription.schema = Joi.object() @@ -928,6 +828,27 @@ getSkillIdsByNames.schema = Joi.object() 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 * @@ -1133,6 +1054,7 @@ module.exports = { getSkillsByJobDescription, getSkillNamesByIds, getSkillIdsByNames, + getSkillNamesByNames, createRoleSearchRequest, isExternalMember, createTeam, From ff8c4e0844ebf48456286918aa5bf740bab875db Mon Sep 17 00:00:00 2001 From: Nikolay Iakovlev Date: Mon, 14 Jun 2021 21:51:45 +0200 Subject: [PATCH 73/86] Revert "Revert "Feature/role jd parser2"" --- config/default.js | 2 + ...coder-bookings-api.postman_collection.json | 255 ++++++-- docs/stopWords.json | 574 ++++++++++++++++++ docs/swagger.yaml | 9 +- src/services/TeamService.js | 130 +++- 5 files changed, 899 insertions(+), 71 deletions(-) create mode 100644 docs/stopWords.json diff --git a/config/default.js b/config/default.js index d90ae6c0..726c40fa 100644 --- a/config/default.js +++ b/config/default.js @@ -176,6 +176,8 @@ module.exports = { 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'], + // Topcoder skills cache time in minutes + TOPCODER_SKILLS_CACHE_TIME: process.env.TOPCODER_SKILLS_CACHE_TIME || 60, // payment scheduler config PAYMENT_PROCESSING: { // switch off actual API calls in Payment Scheduler diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 479ddceb..b37011a5 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "18310e1b-429d-49db-8555-f4a54404271f", + "_postman_id": "d413d21d-272f-454f-b26a-0d7e3bf926d9", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -21165,6 +21165,221 @@ } ] }, + { + "name": "Get Skills by Job Description", + "item": [ + { + "name": "get skills successfully", + "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}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{ \"description\": \"Description A global leading healthcare company is seeking a strong Databricks Engineer to join their development team as they build their new Databricks workspace. Development efforts will contribute to the migration of data from Hadoop to Databricks to prepare data for visualization. Candidate must be well-versed in Databricks components and best practices, be an excellent problem solver and be comfortable working in a fast-moving, rapidly changing, and dynamic environment via Agile, SCRUM, and DevOps. PREFERRED QUALIFICATIONS: 2+ years of Azure Data Stack experience: Azure Data Services using ADF, ADLS, Databricks with PySpark, Azure DevOps & Azure Key Vault. Strong knowledge of various data warehousing methodologies and data modeling concepts. Hands-on experience using Azure, Azure data lake, Azure functions & Databricks Minimum 2-3+ years of Python experience (PySpark) Design & Develop Azure native solutions for Data Platform Minimum 3+ years of experience using Big Data ecosystem (Cloudera/Hortonworks) using Oozie, Hive, Impala, and Spark Expert in SQL and performance tuning\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "getSkillsByJobDescription" + ] + } + }, + "response": [] + }, + { + "name": "get skills by 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", + "type": "text", + "value": "Bearer invalid_token" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{ \"description\": \"Description A global leading healthcare company is seeking a strong Databricks Engineer to join their development team as they build their new Databricks workspace. Development efforts will contribute to the migration of data from Hadoop to Databricks to prepare data for visualization. Candidate must be well-versed in Databricks components and best practices, be an excellent problem solver and be comfortable working in a fast-moving, rapidly changing, and dynamic environment via Agile, SCRUM, and DevOps. PREFERRED QUALIFICATIONS: 2+ years of Azure Data Stack experience: Azure Data Services using ADF, ADLS, Databricks with PySpark, Azure DevOps & Azure Key Vault. Strong knowledge of various data warehousing methodologies and data modeling concepts. Hands-on experience using Azure, Azure data lake, Azure functions & Databricks Minimum 2-3+ years of Python experience (PySpark) Design & Develop Azure native solutions for Data Platform Minimum 3+ years of experience using Big Data ecosystem (Cloudera/Hortonworks) using Oozie, Hive, Impala, and Spark Expert in SQL and performance tuning\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "getSkillsByJobDescription" + ] + } + }, + "response": [] + }, + { + "name": "get skills by invalid 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.description\\\" is not allowed to be empty\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{ \"description\": \"\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "getSkillsByJobDescription" + ] + } + }, + "response": [] + }, + { + "name": "get skills by 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.description\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "getSkillsByJobDescription" + ] + } + }, + "response": [] + } + ] + }, { "name": "GET /taas-teams", "request": { @@ -21648,44 +21863,6 @@ }, "response": [] }, - { - "name": "POST /taas-teams/getSkillsByJobDescription", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_member}}" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"description\": \"nodejs react c++ hello\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/taas-teams/getSkillsByJobDescription", - "host": [ - "{{URL}}" - ], - "path": [ - "taas-teams", - "getSkillsByJobDescription" - ] - } - }, - "response": [] - }, { "name": "GET /taas-teams/:id/members", "request": { diff --git a/docs/stopWords.json b/docs/stopWords.json new file mode 100644 index 00000000..dded6681 --- /dev/null +++ b/docs/stopWords.json @@ -0,0 +1,574 @@ +[ + "dr", + "dra", + "mr", + "ms", + "a", + "a's", + "able", + "about", + "above", + "according", + "accordingly", + "across", + "actually", + "after", + "afterwards", + "again", + "against", + "ain't", + "all", + "allow", + "allows", + "almost", + "alone", + "along", + "already", + "also", + "although", + "always", + "am", + "among", + "amongst", + "an", + "and", + "another", + "any", + "anybody", + "anyhow", + "anyone", + "anything", + "anyway", + "anyways", + "anywhere", + "apart", + "appear", + "appreciate", + "appropriate", + "are", + "aren't", + "around", + "as", + "aside", + "ask", + "asking", + "associated", + "at", + "available", + "away", + "awfully", + "b", + "be", + "became", + "because", + "become", + "becomes", + "becoming", + "been", + "before", + "beforehand", + "behind", + "being", + "believe", + "below", + "beside", + "besides", + "best", + "better", + "between", + "beyond", + "both", + "brief", + "but", + "by", + "c'mon", + "c's", + "came", + "can", + "can't", + "cannot", + "cant", + "cause", + "causes", + "certain", + "certainly", + "changes", + "clearly", + "co", + "come", + "comes", + "concerning", + "consequently", + "consider", + "considering", + "contain", + "containing", + "contains", + "corresponding", + "could", + "couldn't", + "course", + "currently", + "d", + "definitely", + "described", + "despite", + "did", + "didn't", + "different", + "do", + "does", + "doesn't", + "doing", + "don't", + "done", + "down", + "downwards", + "during", + "e", + "each", + "edu", + "eg", + "eight", + "either", + "else", + "elsewhere", + "enough", + "entirely", + "especially", + "et", + "etc", + "even", + "ever", + "every", + "everybody", + "everyone", + "everything", + "everywhere", + "ex", + "exactly", + "example", + "except", + "f", + "far", + "few", + "fifth", + "first", + "five", + "followed", + "following", + "follows", + "for", + "former", + "formerly", + "forth", + "four", + "from", + "further", + "furthermore", + "g", + "get", + "gets", + "getting", + "given", + "gives", + "goes", + "going", + "gone", + "got", + "gotten", + "greetings", + "h", + "had", + "hadn't", + "happens", + "hardly", + "has", + "hasn't", + "have", + "haven't", + "having", + "he", + "he's", + "hello", + "help", + "hence", + "her", + "here", + "here's", + "hereafter", + "hereby", + "herein", + "hereupon", + "hers", + "herself", + "hi", + "him", + "himself", + "his", + "hither", + "hopefully", + "how", + "howbeit", + "however", + "i", + "i'd", + "i'll", + "i'm", + "i've", + "ie", + "if", + "ignored", + "immediate", + "in", + "inasmuch", + "inc", + "indeed", + "indicate", + "indicated", + "indicates", + "inner", + "insofar", + "instead", + "into", + "inward", + "is", + "isn't", + "it", + "it'd", + "it'll", + "it's", + "its", + "itself", + "j", + "just", + "k", + "keep", + "keeps", + "kept", + "know", + "knows", + "known", + "l", + "last", + "lately", + "later", + "latter", + "latterly", + "least", + "lest", + "let", + "let's", + "like", + "liked", + "likely", + "little", + "look", + "looking", + "looks", + "ltd", + "m", + "mainly", + "many", + "may", + "maybe", + "me", + "mean", + "meanwhile", + "merely", + "might", + "more", + "moreover", + "most", + "mostly", + "much", + "must", + "my", + "myself", + "n", + "name", + "namely", + "nd", + "near", + "nearly", + "necessary", + "need", + "needs", + "neither", + "never", + "nevertheless", + "new", + "next", + "nine", + "no", + "nobody", + "non", + "none", + "noone", + "nor", + "normally", + "not", + "nothing", + "novel", + "now", + "nowhere", + "o", + "obviously", + "of", + "off", + "often", + "oh", + "ok", + "okay", + "old", + "on", + "once", + "one", + "ones", + "only", + "onto", + "or", + "other", + "others", + "otherwise", + "ought", + "our", + "ours", + "ourselves", + "out", + "outside", + "over", + "overall", + "own", + "p", + "particular", + "particularly", + "per", + "perhaps", + "placed", + "please", + "plus", + "point", + "possible", + "presumably", + "probably", + "provides", + "q", + "que", + "quite", + "qv", + "rather", + "rd", + "re", + "really", + "reasonably", + "regarding", + "regardless", + "regards", + "relatively", + "respectively", + "right", + "s", + "said", + "same", + "saw", + "say", + "saying", + "says", + "second", + "secondly", + "see", + "seeing", + "seem", + "seemed", + "seeming", + "seems", + "seen", + "self", + "selves", + "sensible", + "sent", + "serious", + "seriously", + "seven", + "several", + "shall", + "she", + "should", + "shouldn't", + "since", + "six", + "so", + "some", + "somebody", + "somehow", + "someone", + "something", + "sometime", + "sometimes", + "somewhat", + "somewhere", + "soon", + "sorry", + "specified", + "specify", + "specifying", + "still", + "strong", + "sub", + "such", + "sup", + "sure", + "t", + "t's", + "take", + "taken", + "tell", + "tends", + "th", + "than", + "thank", + "thanks", + "thanx", + "that", + "that's", + "thats", + "the", + "their", + "theirs", + "them", + "themselves", + "then", + "thence", + "there", + "there's", + "thereafter", + "thereby", + "therefore", + "therein", + "theres", + "thereupon", + "these", + "they", + "they'd", + "they'll", + "they're", + "they've", + "think", + "third", + "this", + "thorough", + "thoroughly", + "those", + "though", + "three", + "through", + "throughout", + "thru", + "thus", + "to", + "together", + "too", + "took", + "toward", + "towards", + "tried", + "tries", + "truly", + "try", + "trying", + "twice", + "two", + "u", + "un", + "under", + "unfortunately", + "unless", + "unlikely", + "until", + "unto", + "up", + "upon", + "us", + "use", + "used", + "useful", + "uses", + "using", + "usually", + "uucp", + "v", + "value", + "various", + "very", + "via", + "viz", + "vs", + "w", + "want", + "wants", + "was", + "wasn't", + "way", + "we", + "we'd", + "we'll", + "we're", + "we've", + "welcome", + "well", + "went", + "were", + "weren't", + "what", + "what's", + "whatever", + "when", + "whence", + "whenever", + "where", + "where's", + "whereafter", + "whereas", + "whereby", + "wherein", + "whereupon", + "wherever", + "whether", + "which", + "while", + "whither", + "who", + "who's", + "whoever", + "whole", + "whom", + "whose", + "why", + "will", + "willing", + "wish", + "with", + "within", + "without", + "won't", + "wonder", + "would", + "would", + "wouldn't", + "x", + "y", + "yes", + "yet", + "you", + "you'd", + "you'll", + "you're", + "you've", + "your", + "yours", + "yourself", + "yourselves", + "z", + "zero" +] \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index cab269c9..e6cb5988 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3279,12 +3279,6 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - "403": - description: Forbidden - content: - application/json: - schema: - $ref: "#/components/schemas/Error" "500": description: Internal Server Error content: @@ -3765,10 +3759,13 @@ components: properties: tag: type: string + example: "Java" type: type: string + example: "taas_skill" source: type: string + example: "taas-jd-parser" Job: required: diff --git a/src/services/TeamService.js b/src/services/TeamService.js index c32b2776..af006603 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -15,8 +15,10 @@ const ResourceBookingService = require('./ResourceBookingService') const HttpStatus = require('http-status-codes') const { Op } = require('sequelize') const models = require('../models') +const stopWords = require('../../docs/stopWords.json') const Role = models.Role const RoleSearchRequest = models.RoleSearchRequest +const topcoderSkills = {} const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { return { @@ -60,6 +62,73 @@ async function _getJobsByProjectIds (currentUser, projectIds) { return result } +/** + * Gets topcoder skills and stores their name and compiled + * regex patters according to Levenshtein distance <=1 + */ +async function _reloadCachedTopcoderSkills () { + // do not reload if cache time is not expired + if (!_.isUndefined(topcoderSkills.time)) { + const cacheTime = config.TOPCODER_SKILLS_CACHE_TIME * 60 * 1000 + if (new Date().getTime() - topcoderSkills.time < cacheTime) { + return + } + } + // collect all skills + const skills = await helper.getAllTopcoderSkills() + // set the last cached time + topcoderSkills.time = new Date().getTime() + topcoderSkills.skills = [] + // store skill names and compiled regex paterns + _.each(skills, skill => { + topcoderSkills.skills.push({ + name: skill.name, + pattern: _compileRegexPatternForSkillName(skill.name) + }) + }) +} + +/** + * Prepares the regex pattern for the given word + * according to Levenshtein distance of 1 (insertions, deletions or substitutions) + * @param {String} skillName the name of the skill + * @returns {RegExp} the compiled regex pattern + */ +function _compileRegexPatternForSkillName (skillName) { + // split the name into its chars + let chars = _.split(skillName, '') + // escape characters reserved to regex + chars = _.map(chars, _.escapeRegExp) + // Its not a good idea to apply tolerance according to + // Levenshtein distance for the words have less than 3 letters + // We expect the skill names have 1 or 2 letters to take place + // in job description as how they are exactly spelled + if (chars.length < 3) { + return new RegExp(`^(?:${_.join(chars, '')})$`, 'i') + } + + const res = [] + // include the skill name itself + res.push(_.join(chars, '')) + // include every transposition combination + // E.g. java => ajva, jvaa, jaav + for (let i = 0; i < chars.length - 1; i++) { + res.push(_.join(_.slice(chars, 0, i), '') + chars[i + 1] + chars[i] + _.join(_.slice(chars, i + 2), '')) + } + // include every insertion combination + // E.g. java => .java, j.ava, ja.va, jav.a, java. + for (let i = 0; i <= chars.length; i++) { + res.push(_.join(_.slice(chars, 0, i), '') + '.' + _.join(_.slice(chars, i), '')) + } + // include every deletion/substitution combination + // E.g. java => .?ava, j.?va, ja.?a, jav.? + for (let i = 0; i < chars.length; i++) { + res.push(_.join(_.slice(chars, 0, i), '') + '.?' + _.join(_.slice(chars, i + 1), '')) + } + // return the regex pattern + return new RegExp(`^(?:${_.join(res, '|')})$`, 'i') +} + /** * List teams * @param {Object} currentUser the user who perform this operation @@ -695,9 +764,7 @@ async function roleSearchRequest (currentUser, data) { } 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')) + const skills = _.map(tags, 'tag') // find the best matching role role = await getRoleBySkills(skills) } @@ -763,7 +830,40 @@ getRoleBySkills.schema = Joi.object() * @returns {Object} the result */ async function getSkillsByJobDescription (currentUser, data) { - return helper.getTags(data.description) + // load topcoder skills if needed. Using cached skills helps to avoid + // unnecessary api calls which is extremely time comsuming. + await _reloadCachedTopcoderSkills() + // replace markdown tags with spaces + let description = _.replace(data.description, /[`|^[\]{}~/,:-]|#{2,}|
/gi, ' ') + // replace all whitespace characters with single space + description = _.replace(description, /\s\s+/g, ' ') + // extract words from description + let words = _.split(description, ' ') + // remove stopwords from description + words = _.filter(words, word => stopWords.indexOf(word.toLowerCase()) === -1) + let foundSkills = [] + const result = [] + // try to match each word with skill names + // using pre-compiled regex pattern + _.each(words, word => { + _.each(topcoderSkills.skills, skill => { + // do not stop searching after a match in order to detect more lookalikes + if (skill.pattern.test(word)) { + foundSkills.push(skill.name) + } + }) + }) + foundSkills = _.uniq(foundSkills) + // apply desired template + _.each(foundSkills, skill => { + result.push({ + tag: skill, + type: 'taas_skill', + source: 'taas-jd-parser' + }) + }) + + return result } getSkillsByJobDescription.schema = Joi.object() @@ -828,27 +928,6 @@ getSkillIdsByNames.schema = Joi.object() 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 * @@ -1054,7 +1133,6 @@ module.exports = { getSkillsByJobDescription, getSkillNamesByIds, getSkillIdsByNames, - getSkillNamesByNames, createRoleSearchRequest, isExternalMember, createTeam, From 9db37984160c686bdb14642b477ede56beb8e18b Mon Sep 17 00:00:00 2001 From: eisbilir Date: Mon, 14 Jun 2021 23:58:50 +0300 Subject: [PATCH 74/86] use paymentStatus constats --- app-constants.js | 10 +++++++++ data/demo-data.json | 10 ++++----- docs/swagger.yaml | 6 ++--- ...late-work-periods-for-resource-bookings.js | 3 ++- src/bootstrap.js | 4 ++-- .../ResourceBookingEventHandler.js | 3 ++- .../WorkPeriodPaymentEventHandler.js | 21 +++++++++--------- src/services/ResourceBookingService.js | 8 +++---- src/services/WorkPeriodPaymentService.js | 14 +++++++++--- src/services/WorkPeriodService.js | 22 +++++++++---------- test/unit/common/ResourceBookingData.js | 14 ++++++------ 11 files changed, 68 insertions(+), 47 deletions(-) diff --git a/app-constants.js b/app-constants.js index 701df82d..a09f57aa 100644 --- a/app-constants.js +++ b/app-constants.js @@ -84,6 +84,15 @@ const ChallengeStatus = { COMPLETED: 'Completed' } +const PaymentStatus = { + PENDING: 'pending', + IN_PROGRESS: 'in-progress', + PARTIALLY_COMPLETED: 'partially-completed', + COMPLETED: 'completed', + FAILED: 'failed', + NO_DAYS: 'no-days' +} + const WorkPeriodPaymentStatus = { COMPLETED: 'completed', SCHEDULED: 'scheduled', @@ -117,6 +126,7 @@ module.exports = { Scopes, Interviews, ChallengeStatus, + PaymentStatus, WorkPeriodPaymentStatus, WorkPeriodPaymentUpdateStatus, PaymentSchedulerStatus, diff --git a/data/demo-data.json b/data/demo-data.json index f390ef15..ce9f2456 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -1533,7 +1533,7 @@ "daysWorked": 0, "daysPaid": 0, "paymentTotal": 0, - "paymentStatus": "noDays", + "paymentStatus": "no-days", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c", "createdAt": "2021-05-09T21:27:12.794Z", @@ -2430,7 +2430,7 @@ "daysWorked": 0, "daysPaid": 0, "paymentTotal": 0, - "paymentStatus": "noDays", + "paymentStatus": "no-days", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-30T11:48:57.210Z", @@ -2551,7 +2551,7 @@ "daysWorked": 0, "daysPaid": 0, "paymentTotal": 0, - "paymentStatus": "noDays", + "paymentStatus": "no-days", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-30T11:49:18.405Z", @@ -2914,7 +2914,7 @@ "daysWorked": 0, "daysPaid": 0, "paymentTotal": 0, - "paymentStatus": "noDays", + "paymentStatus": "no-days", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-30T11:49:01.820Z", @@ -4644,7 +4644,7 @@ "daysWorked": 0, "daysPaid": 0, "paymentTotal": 0, - "paymentStatus": "noDays", + "paymentStatus": "no-days", "createdBy": "00000000-0000-0000-0000-000000000000", "updatedBy": "00000000-0000-0000-0000-000000000000", "createdAt": "2021-05-30T11:49:20.193Z", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 21f6e9cf..984783ec 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1923,7 +1923,7 @@ paths: "completed", "in-progress", "failed", - "noDays", + "no-days", ] - type: string enum: @@ -1933,7 +1933,7 @@ paths: "completed", "in-progress", "failed", - "noDays", + "no-days", ] description: comma separated payment status. - in: query @@ -4542,7 +4542,7 @@ components: "completed", "in-progress", "failed", - "noDays", + "no-days", ] description: "The payment status." payments: diff --git a/migrations/2021-06-14-create-and-populate-work-periods-for-resource-bookings.js b/migrations/2021-06-14-create-and-populate-work-periods-for-resource-bookings.js index dcda9f36..7e484f4f 100644 --- a/migrations/2021-06-14-create-and-populate-work-periods-for-resource-bookings.js +++ b/migrations/2021-06-14-create-and-populate-work-periods-for-resource-bookings.js @@ -3,6 +3,7 @@ const ResourceBooking = require('../src/models').ResourceBooking const _ = require('lodash') const helper = require('../src/common/helper') const { v4: uuid } = require('uuid') +const { PaymentStatus } = require('../app-constants') // maximum start date of resource bookings when populating work periods from existing resource bookings in migration script const MAX_START_DATE = process.env.MAX_START_DATE || '2100-12-31' @@ -84,7 +85,7 @@ module.exports = { days_worked: period.daysWorked, days_paid: 0, payment_total: 0, - payment_status: period.daysWorked === 0 ? 'noDays' : 'pending', + payment_status: period.daysWorked === 0 ? PaymentStatus.NO_DAYS : PaymentStatus.PENDING, created_by: config.m2m.M2M_AUDIT_USER_ID, created_at: new Date() }) diff --git a/src/bootstrap.js b/src/bootstrap.js index 7230bedf..b2f094b0 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -3,7 +3,7 @@ const Joi = require('joi') const config = require('config') const path = require('path') const _ = require('lodash') -const { Interviews, WorkPeriodPaymentStatus, WorkPeriodPaymentUpdateStatus, PaymentProcessingSwitch } = require('../app-constants') +const { Interviews, PaymentStatus, WorkPeriodPaymentStatus, WorkPeriodPaymentUpdateStatus, PaymentProcessingSwitch } = require('../app-constants') const logger = require('./common/logger') const allowedInterviewStatuses = _.values(Interviews.Status) @@ -17,7 +17,7 @@ Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancel Joi.workload = () => Joi.string().valid('full-time', 'fractional') Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed', 'offered') Joi.title = () => Joi.string().max(128) -Joi.paymentStatus = () => Joi.string().valid('pending', 'in-progress', 'partially-completed', 'completed', 'failed', 'noDays') +Joi.paymentStatus = () => Joi.string().valid(..._.values(PaymentStatus)) Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) Joi.interviewStatus = () => Joi.string().valid(...allowedInterviewStatuses) Joi.workPeriodPaymentStatus = () => Joi.string().valid(..._.values(WorkPeriodPaymentStatus)) diff --git a/src/eventHandlers/ResourceBookingEventHandler.js b/src/eventHandlers/ResourceBookingEventHandler.js index 96a43fec..3ecbbc02 100644 --- a/src/eventHandlers/ResourceBookingEventHandler.js +++ b/src/eventHandlers/ResourceBookingEventHandler.js @@ -7,6 +7,7 @@ const _ = require('lodash') const models = require('../models') const logger = require('../common/logger') const helper = require('../common/helper') +const { PaymentStatus } = require('../../app-constants') const JobService = require('../services/JobService') const JobCandidateService = require('../services/JobCandidateService') const WorkPeriodService = require('../services/WorkPeriodService') @@ -295,7 +296,7 @@ async function _createWorkPeriods (periods, resourceBookingId) { startDate: period.startDate, endDate: period.endDate, daysWorked: period.daysWorked, - paymentStatus: period.daysWorked === 0 ? 'noDays' : 'pending' + paymentStatus: period.daysWorked === 0 ? PaymentStatus.NO_DAYS : PaymentStatus.PENDING }) } } diff --git a/src/eventHandlers/WorkPeriodPaymentEventHandler.js b/src/eventHandlers/WorkPeriodPaymentEventHandler.js index a3913b15..0da5163e 100644 --- a/src/eventHandlers/WorkPeriodPaymentEventHandler.js +++ b/src/eventHandlers/WorkPeriodPaymentEventHandler.js @@ -7,6 +7,7 @@ const config = require('config') const models = require('../models') const logger = require('../common/logger') const helper = require('../common/helper') +const { PaymentStatus, WorkPeriodPaymentStatus } = require('../../app-constants') const WorkPeriod = models.WorkPeriod /** @@ -26,23 +27,23 @@ async function updateWorkPeriod (payload) { data.paymentTotal = 0 _.each(workPeriod.payments, payment => { paymentStatuses[payment.status] = true - if (_.includes(['scheduled', 'in-progress', 'completed'], payment.status)) { + if (_.includes([WorkPeriodPaymentStatus.SCHEDULED, WorkPeriodPaymentStatus.IN_PROGRESS, WorkPeriodPaymentStatus.COMPLETED], payment.status)) { data.daysPaid += payment.days data.paymentTotal += payment.amount } }) if (workPeriod.daysWorked === 0) { - data.paymentStatus = 'noDays' - } else if (paymentStatuses.scheduled || paymentStatuses['in-progress']) { - data.paymentStatus = 'in-progress' + data.paymentStatus = PaymentStatus.NO_DAYS + } else if (paymentStatuses[WorkPeriodPaymentStatus.SCHEDULED] || paymentStatuses[WorkPeriodPaymentStatus.IN_PROGRESS]) { + data.paymentStatus = PaymentStatus.IN_PROGRESS } else if (workPeriod.daysWorked === data.daysPaid) { - data.paymentStatus = 'completed' - } else if (paymentStatuses.completed) { - data.paymentStatus = 'partially-completed' - } else if (paymentStatuses.failed) { - data.paymentStatus = 'failed' + data.paymentStatus = PaymentStatus.COMPLETED + } else if (paymentStatuses[WorkPeriodPaymentStatus.COMPLETED]) { + data.paymentStatus = PaymentStatus.PARTIALLY_COMPLETED + } else if (paymentStatuses[WorkPeriodPaymentStatus.FAILED]) { + data.paymentStatus = PaymentStatus.FAILED } else { - data.paymentStatus = 'pending' + data.paymentStatus = PaymentStatus.PENDING } if (workPeriod.daysPaid === data.daysPaid && workPeriod.paymentTotal === data.paymentTotal && workPeriod.paymentStatus === data.paymentStatus) { logger.debug({ diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 5cd2e705..cb70b01f 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -171,12 +171,12 @@ async function _ensurePaidWorkPeriodsNotDeleted (resourceBookingId, oldValue, ne function _checkForPaidWorkPeriods (workPeriods) { const paidWorkPeriods = _.filter(workPeriods, workPeriod => { // filter by WP and WPP status - return (['completed', 'partially-completed', 'in-progress'].indexOf(workPeriod.paymentStatus) !== -1 || - _.some(workPeriod.payments, payment => ['completed', 'in-progress', 'shceduled'].indexOf(payment.status) !== -1)) + return ([constants.PaymentStatus.COMPLETED, constants.PaymentStatus.PARTIALLY_COMPLETED, constants.PaymentStatus.IN_PROGRESS].indexOf(workPeriod.paymentStatus) !== -1 || + _.some(workPeriod.payments, payment => [constants.WorkPeriodPaymentStatus.COMPLETED, constants.WorkPeriodPaymentStatus.IN_PROGRESS, constants.WorkPeriodPaymentStatus.SCHEDULED].indexOf(payment.status) !== -1)) }) if (paidWorkPeriods.length > 0) { throw new errors.BadRequestError(`WorkPeriods with id of ${_.map(paidWorkPeriods, workPeriod => workPeriod.id)} - has completed, partially-completed or in-progress payment status.`) + has ${constants.PaymentStatus.COMPLETED}, ${constants.PaymentStatus.PARTIALLY_COMPLETED} or ${constants.PaymentStatus.IN_PROGRESS} payment status.`) } } // find related workPeriods to evaluate the changes @@ -700,7 +700,7 @@ searchResourceBookings.schema = Joi.object().keys({ page: Joi.page(), perPage: Joi.perPage(), sortBy: Joi.string().valid('id', 'rateType', 'startDate', 'endDate', 'customerRate', 'memberRate', 'status', - 'workPeriods.userHandle', 'workPeriods.daysWorked', 'workPeriods.customerRate', 'workPeriods.memberRate', 'workPeriods.paymentStatus'), + 'workPeriods.userHandle', 'workPeriods.daysWorked', 'workPeriods.daysPaid', 'workPeriods.paymentTotal', 'workPeriods.paymentStatus'), sortOrder: Joi.string().valid('desc', 'asc'), status: Joi.resourceBookingStatus(), startDate: Joi.date().format('YYYY-MM-DD'), diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index cad84da3..b141df2d 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -12,7 +12,7 @@ const helper = require('../common/helper') const logger = require('../common/logger') const errors = require('../common/errors') const models = require('../models') -const { WorkPeriodPaymentStatus } = require('../../app-constants') +const { PaymentStatus, WorkPeriodPaymentStatus } = require('../../app-constants') const { searchResourceBookings } = require('./ResourceBookingService') const WorkPeriodPayment = models.WorkPeriodPayment @@ -63,6 +63,9 @@ async function _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking (w if (_.isNil(correspondingResourceBooking.memberRate)) { throw new errors.ConflictError(`Can't find a member rate in ResourceBooking: ${correspondingResourceBooking.id} to calculate the amount`) } + if (correspondingResourceBooking.memberRate <= 0) { + throw new errors.ConflictError(`Can't process payment with member rate: ${correspondingResourceBooking.memberRate}. It must be higher than 0`) + } workPeriodPayment.memberRate = correspondingResourceBooking.memberRate const maxPossibleDays = correspondingWorkPeriod.daysWorked - correspondingWorkPeriod.daysPaid if (workPeriodPayment.days > maxPossibleDays) { @@ -380,7 +383,9 @@ async function createQueryWorkPeriodPayments (currentUser, criteria) { _checkUserPermissionForCRUWorkPeriodPayment(currentUser) const createdBy = await helper.getUserId(currentUser.userId) const query = criteria.query - + if ((typeof query['workPeriods.paymentStatus']) === 'string') { + query['workPeriods.paymentStatus'] = query['workPeriods.paymentStatus'].trim().split(',').map(ps => Joi.attempt({ paymentStatus: ps.trim() }, Joi.object().keys({ paymentStatus: Joi.string().valid(PaymentStatus.PENDING, PaymentStatus.PARTIALLY_COMPLETED, PaymentStatus.FAILED) })).paymentStatus) + } const fields = _.join(_.uniq(_.concat( ['id', 'billingAccountId', 'memberRate', 'customerRate', 'workPeriods.id', 'workPeriods.resourceBookingId', 'workPeriods.daysWorked', 'workPeriods.daysPaid'], _.map(_.keys(query), k => k === 'projectIds' ? 'projectId' : k)) @@ -418,7 +423,10 @@ createQueryWorkPeriodPayments.schema = Joi.object().keys({ Joi.string(), Joi.array().items(Joi.number().integer()) ), - 'workPeriods.paymentStatus': Joi.string().valid('pending', 'partially-completed', 'failed'), + 'workPeriods.paymentStatus': Joi.alternatives( + Joi.string(), + Joi.array().items(Joi.string().valid(PaymentStatus.PENDING, PaymentStatus.PARTIALLY_COMPLETED, PaymentStatus.FAILED)) + ), 'workPeriods.startDate': Joi.date().format('YYYY-MM-DD'), 'workPeriods.endDate': Joi.date().format('YYYY-MM-DD'), 'workPeriods.userHandle': Joi.string() diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index 33802c77..672062b5 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -279,14 +279,14 @@ async function updateWorkPeriod (currentUser, id, data) { if (thisWeek.daysWorked < data.daysWorked) { throw new errors.BadRequestError(`Maximum allowed daysWorked is (${thisWeek.daysWorked})`) } - if (data.daysWorked > oldValue.daysWorked && oldValue.paymentStatus === 'completed') { - data.paymentStatus = 'partially-completed' - } else if (data.daysWorked > oldValue.daysWorked && oldValue.paymentStatus === 'noDays') { - data.paymentStatus = 'pending' - } else if (data.daysWorked === oldValue.daysPaid && _.includes(['partially-completed', 'failed'], oldValue.paymentStatus)) { - data.paymentStatus = 'completed' + if (data.daysWorked > oldValue.daysWorked && oldValue.paymentStatus === constants.PaymentStatus.COMPLETED) { + data.paymentStatus = constants.PaymentStatus.PARTIALLY_COMPLETED + } else if (data.daysWorked > oldValue.daysWorked && oldValue.paymentStatus === constants.PaymentStatus.NO_DAYS) { + data.paymentStatus = constants.PaymentStatus.PENDING + } else if (data.daysWorked === oldValue.daysPaid && _.includes([constants.PaymentStatus.PARTIALLY_COMPLETED, constants.PaymentStatus.FAILED], oldValue.paymentStatus)) { + data.paymentStatus = constants.PaymentStatus.COMPLETED } else if (data.daysWorked === 0) { - data.paymentStatus = 'noDays' + data.paymentStatus = constants.PaymentStatus.NO_DAYS } data.updatedBy = await helper.getUserId(currentUser.userId) const updated = await workPeriod.update(data) @@ -319,11 +319,11 @@ partiallyUpdateWorkPeriod.schema = Joi.object().keys({ */ async function deleteWorkPeriod (id) { const workPeriod = await WorkPeriod.findById(id, { withPayments: true }) - if (_.includes(['completed', 'partially-completed', 'in-progress'], workPeriod.paymentStatus)) { - throw new errors.BadRequestError("Can't delete WorkPeriod with paymentStatus completed partially-completed, or in-progress") + if (_.includes([constants.PaymentStatus.COMPLETED, constants.PaymentStatus.PARTIALLY_COMPLETED, constants.PaymentStatus.IN_PROGRESS], workPeriod.paymentStatus)) { + throw new errors.BadRequestError(`Can't delete WorkPeriod with paymentStatus ${constants.PaymentStatus.COMPLETED}, ${constants.PaymentStatus.PARTIALLY_COMPLETED}, or ${constants.PaymentStatus.IN_PROGRESS}`) } - if (_.some(workPeriod.payments, payment => ['completed', 'in-progress', 'shceduled'].indexOf(payment.status) !== -1)) { - throw new errors.BadRequestError("Can't delete WorkPeriod if any associated WorkPeriodsPayment has status completed, shceduled or in-progress") + if (_.some(workPeriod.payments, payment => [constants.WorkPeriodPaymentStatus.COMPLETED, constants.WorkPeriodPaymentStatus.IN_PROGRESS, constants.WorkPeriodPaymentStatus.SCHEDULED].indexOf(payment.status) !== -1)) { + throw new errors.BadRequestError(`Can't delete WorkPeriod if any associated WorkPeriodsPayment has status ${constants.WorkPeriodPaymentStatus.COMPLETED}, ${constants.WorkPeriodPaymentStatus.SCHEDULED} or ${constants.WorkPeriodPaymentStatus.IN_PROGRESS}`) } await models.WorkPeriodPayment.destroy({ where: { diff --git a/test/unit/common/ResourceBookingData.js b/test/unit/common/ResourceBookingData.js index de98f771..67cfbd9e 100644 --- a/test/unit/common/ResourceBookingData.js +++ b/test/unit/common/ResourceBookingData.js @@ -35,7 +35,7 @@ const T01 = { startDate: '2021-03-28', endDate: '2021-04-03', daysWorked: 0, - paymentStatus: 'noDays' + paymentStatus: 'no-days' }, { resourceBookingId: '520bb632-a02a-415e-9857-93b2ecbf7d60', @@ -70,7 +70,7 @@ const T01 = { startDate: '2021-05-02', endDate: '2021-05-08', daysWorked: 0, - paymentStatus: 'noDays' + paymentStatus: 'no-days' }] } } @@ -339,7 +339,7 @@ const T07 = { startDate: '2021-04-04', endDate: '2021-04-10', daysWorked: 0, - paymentStatus: 'noDays' + paymentStatus: 'no-days' } }, { @@ -422,7 +422,7 @@ const T08 = { startDate: '2021-04-18', endDate: '2021-04-24', daysWorked: 0, - paymentStatus: 'noDays' + paymentStatus: 'no-days' } } ] @@ -484,7 +484,7 @@ const T09 = { daysWorked: 0, daysPaid: 0, paymentTotal: 0, - paymentStatus: 'noDays', + paymentStatus: 'no-days', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, createdAt: '2021-04-10T22:25:08.289Z', @@ -579,7 +579,7 @@ const T10 = { daysWorked: 0, daysPaid: 0, paymentTotal: 0, - paymentStatus: 'noDays', + paymentStatus: 'no-days', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, createdAt: '2021-04-10T22:25:08.289Z', @@ -674,7 +674,7 @@ const T11 = { daysWorked: 0, daysPaid: 0, paymentTotal: 0, - paymentStatus: 'noDays', + paymentStatus: 'no-days', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, createdAt: '2021-04-10T22:25:08.289Z', From d8bcc78bb3f0bdacfa8fac44bc0a7eb9015fe55b Mon Sep 17 00:00:00 2001 From: eisbilir Date: Tue, 15 Jun 2021 00:20:33 +0300 Subject: [PATCH 75/86] update postman --- ...coder-bookings-api.postman_collection.json | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 7f1ac21e..93a4f25e 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -8734,7 +8734,7 @@ } ], "url": { - "raw": "{{URL}}/resourceBookings?page=1&perPage=10&sortBy=endDate&sortOrder=asc&fields=id,endDate,workPeriods", + "raw": "{{URL}}/resourceBookings?page=1&perPage=10&sortBy=endDate&sortOrder=asc", "host": [ "{{URL}}" ], @@ -8875,7 +8875,8 @@ }, { "key": "fields", - "value": "id,endDate,workPeriods" + "value": "id,endDate,workPeriods", + "disabled": true } ] } @@ -9565,7 +9566,7 @@ "response": [] }, { - "name": "search RB sortBy workPeriods.customerRate", + "name": "search RB sortBy workPeriods.daysPaid", "event": [ { "listen": "test", @@ -9589,7 +9590,7 @@ } ], "url": { - "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=workPeriods.customerRate&workPeriods.startDate=2021-01-03&sortOrder=asc&fields=id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate", + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=workPeriods.daysPaid&workPeriods.startDate=2021-01-03&sortOrder=asc&fields=id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid", "host": [ "{{URL}}" ], @@ -9607,7 +9608,7 @@ }, { "key": "sortBy", - "value": "workPeriods.customerRate" + "value": "workPeriods.daysPaid" }, { "key": "workPeriods.startDate", @@ -9624,7 +9625,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate" + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid" }, { "key": "status", @@ -9633,7 +9634,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,status", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,status", "disabled": true }, { @@ -9643,7 +9644,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,startDate", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,startDate", "disabled": true }, { @@ -9653,7 +9654,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,endDate", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,endDate", "disabled": true }, { @@ -9663,7 +9664,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,rateType", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,rateType", "disabled": true }, { @@ -9673,7 +9674,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,jobId", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,jobId", "disabled": true }, { @@ -9688,7 +9689,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,projectId", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,projectId", "disabled": true }, { @@ -9698,7 +9699,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,workPeriods.paymentStatus", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,workPeriods.paymentStatus", "disabled": true }, { @@ -9708,7 +9709,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,workPeriods.endDate", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,workPeriods.endDate", "disabled": true }, { @@ -9718,7 +9719,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.customerRate,workPeriods.userHandle", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.daysPaid,workPeriods.userHandle", "disabled": true }, { @@ -9732,7 +9733,7 @@ "response": [] }, { - "name": "search RB sortBy workPeriods.memberRate", + "name": "search RB sortBy workPeriods.paymentTotal", "event": [ { "listen": "test", @@ -9756,7 +9757,7 @@ } ], "url": { - "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=workPeriods.memberRate&workPeriods.startDate=2021-01-03&sortOrder=asc&fields=id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate", + "raw": "{{URL}}/resourceBookings?page=1&perPage=5&sortBy=workPeriods.paymentTotal&workPeriods.startDate=2021-01-03&sortOrder=asc&fields=id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal", "host": [ "{{URL}}" ], @@ -9774,7 +9775,7 @@ }, { "key": "sortBy", - "value": "workPeriods.memberRate" + "value": "workPeriods.paymentTotal" }, { "key": "workPeriods.startDate", @@ -9791,7 +9792,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate" + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal" }, { "key": "status", @@ -9800,7 +9801,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,status", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,status", "disabled": true }, { @@ -9810,7 +9811,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,startDate", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,startDate", "disabled": true }, { @@ -9820,7 +9821,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,endDate", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,endDate", "disabled": true }, { @@ -9830,7 +9831,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,rateType", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,rateType", "disabled": true }, { @@ -9840,7 +9841,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,jobId", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,jobId", "disabled": true }, { @@ -9855,7 +9856,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,projectId", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,projectId", "disabled": true }, { @@ -9865,7 +9866,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,workPeriods.paymentStatus", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,workPeriods.paymentStatus", "disabled": true }, { @@ -9875,7 +9876,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,workPeriods.endDate", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,workPeriods.endDate", "disabled": true }, { @@ -9885,7 +9886,7 @@ }, { "key": "fields", - "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.memberRate,workPeriods.userHandle", + "value": "id,workPeriods.id,workPeriods.startDate,workPeriods.paymentTotal,workPeriods.userHandle", "disabled": true }, { @@ -15049,7 +15050,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(\"Days cannot be more than not paid days which is 0\")\r", + " pm.expect(response.message).to.eq(\"Days cannot be more than not paid days which is 3\")\r", "});" ], "type": "text/javascript" @@ -15456,7 +15457,7 @@ " if(pm.response.status === \"OK\"){\r", " const response = pm.response.json()\r", " pm.environment.set(\"workPeriodPaymentId-2\", response[0].id);\r", - " pm.environment.set(\"workPeriodPaymentId-3\", response[0].id);\r", + " pm.environment.set(\"workPeriodPaymentId-3\", response[1].id);\r", " }\r", "});" ], From e66100c8faa43ab376abc585f1792d7f0c685ed0 Mon Sep 17 00:00:00 2001 From: urwithat Date: Tue, 15 Jun 2021 11:59:40 +0530 Subject: [PATCH 76/86] Moved file stopWords.json from docs to data --- {docs => data}/stopWords.json | 0 src/services/TeamService.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {docs => data}/stopWords.json (100%) diff --git a/docs/stopWords.json b/data/stopWords.json similarity index 100% rename from docs/stopWords.json rename to data/stopWords.json diff --git a/src/services/TeamService.js b/src/services/TeamService.js index af006603..566262a3 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -15,7 +15,7 @@ const ResourceBookingService = require('./ResourceBookingService') const HttpStatus = require('http-status-codes') const { Op } = require('sequelize') const models = require('../models') -const stopWords = require('../../docs/stopWords.json') +const stopWords = require('../../data/stopWords.json') const Role = models.Role const RoleSearchRequest = models.RoleSearchRequest const topcoderSkills = {} From a5cba61741109e906a43acf7fb1390bb30f514f4 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Tue, 15 Jun 2021 21:12:26 +0300 Subject: [PATCH 77/86] improve jd parser --- src/common/helper.js | 53 ++++++++++++++++++++++++++++++++++++- src/services/TeamService.js | 10 ++++--- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 45e4afe4..b3dea59b 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1903,6 +1903,56 @@ async function getMemberGroups (userId) { return _.get(res, 'body') } +/** + * Removes markdown and html formatting from given text + * + * @param {String} text formatted text + * @returns {String} cleaned words seperated by single space + */ +function removeTextFormatting (text) { + text = _.replace(text, /^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm, ' ') + text = _.replace(text, /^([\s\t]*)([*\-+]|\d+\.)\s+/gm, ' $1 ') + // Header + text = _.replace(text, /\n={2,}/g, '\n') + // Fenced codeblocks + text = _.replace(text, /~{3}.*\n/g, ' ') + // Strikethrough + text = _.replace(text, /~~/g, ' ') + // Fenced codeblocks + text = _.replace(text, /`{3}.*\n/g, ' ') + // Remove HTML tags + text = _.replace(text, /<[^>]*>/g, ' ') + // Remove setext-style headers + text = _.replace(text, /^[=-]{2,}\s*$/g, ' ') + // Remove footnotes + text = _.replace(text, /\[\^.+?\](: .*?$)?/g, ' ') + text = _.replace(text, /\s{0,2}\[.*?\]: .*?$/g, ' ') + // Remove images + text = _.replace(text, /!\[(.*?)\][[(].*?[\])]/g, ' $1 ') + // Remove inline links + text = _.replace(text, /\[(.*?)\][[(].*?[\])]/g, ' $1 ') + // Remove blockquotes + text = _.replace(text, /^\s{0,3}>\s?/g, ' ') + // Remove reference-style links + text = _.replace(text, /^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, ' ') + // Remove atx-style headers + text = _.replace(text, /^#{1,6}\s*([^#]*)\s*#{1,6}?$/gm, ' $1 ') + // Remove emphasis (repeat the line to remove double emphasis) + text = _.replace(text, /([*_]{1,3})(\S.*?\S{0,1})\1/g, ' $2 ') + text = _.replace(text, /([*_]{1,3})(\S.*?\S{0,1})\1/g, ' $2 ') + // Remove code blocks + text = _.replace(text, /(`{3,})(.*?)\1/gm, ' $2 ') + // Remove inline code + text = _.replace(text, /`(.+?)`/g, ' $1 ') + // Remove punctuation + text = _.replace(text, /[,"'?/\\]/g, ' ') + // Replace two or more newlines + text = _.replace(text, /\n/g, ' ') + // replace all whitespace characters with single space + text = _.replace(text, /\s\s+/g, ' ') + return text +} + module.exports = { getParamFromCliArgs, promptUser, @@ -1961,5 +2011,6 @@ module.exports = { getUserByHandle, substituteStringByObject, createProject, - getMemberGroups + getMemberGroups, + removeTextFormatting } diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 566262a3..867d958b 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -834,13 +834,17 @@ async function getSkillsByJobDescription (currentUser, data) { // unnecessary api calls which is extremely time comsuming. await _reloadCachedTopcoderSkills() // replace markdown tags with spaces - let description = _.replace(data.description, /[`|^[\]{}~/,:-]|#{2,}|
/gi, ' ') - // replace all whitespace characters with single space - description = _.replace(description, /\s\s+/g, ' ') + const description = helper.removeTextFormatting(data.description) // extract words from description let words = _.split(description, ' ') // remove stopwords from description words = _.filter(words, word => stopWords.indexOf(word.toLowerCase()) === -1) + // include consecutive two word combinations + const twoWords = [] + for (let i = 0; i < words.length - 1; i++) { + twoWords.push(`${words[i]} ${words[i + 1]}`) + } + words = _.concat(words, twoWords) let foundSkills = [] const result = [] // try to match each word with skill names From 04af7f14fb45da10a4d14e98693a66d65b8b85e6 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 16 Jun 2021 01:32:24 +0300 Subject: [PATCH 78/86] payment status calculation --- src/common/helper.js | 40 +++++++++++++++++++ .../ResourceBookingEventHandler.js | 8 ++-- .../WorkPeriodPaymentEventHandler.js | 18 +-------- src/services/ResourceBookingService.js | 2 +- src/services/WorkPeriodService.js | 13 ++---- test/unit/common/WorkPeriodPaymentData.js | 10 ++--- 6 files changed, 56 insertions(+), 35 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 3eb852c0..9dfd9f23 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -21,6 +21,7 @@ const models = require('../models') const eventDispatcher = require('./eventDispatcher') const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') const moment = require('moment') +const { PaymentStatus, WorkPeriodPaymentStatus } = require('../../app-constants') const localLogger = { debug: (message) => @@ -1808,6 +1809,44 @@ function extractWorkPeriods (start, end) { return periods } +/** + * Calculate the payment status of given workPeriod + * @param {object} workPeriod workPeriod object with payments + * @returns {string} new workperiod payment status + * @throws {ConflictError} when no rule matches + */ +function calculateWorkPeriodPaymentStatus (workPeriod) { + function matchRule (rule) { + const actualState = { + daysWorked: workPeriod.daysWorked, + hasDueDays: workPeriod.daysWorked > workPeriod.daysPaid + } + return _.every(_.keys(rule.condition), condition => { + if (_.isArray(rule.condition[condition])) { + return checkIfExists(_.map(workPeriod.payments, 'status'), rule.condition[condition]) + } else { + return rule.condition[condition] === actualState[condition] + } + }) + } + // define rules for the payment status as constant + const PAYMENT_STATUS_RULES = [ + { paymentStatus: PaymentStatus.NO_DAYS, condition: { daysWorked: 0 } }, + { paymentStatus: PaymentStatus.IN_PROGRESS, condition: { hasWorkPeriodPaymentStatus: [WorkPeriodPaymentStatus.SCHEDULED, WorkPeriodPaymentStatus.IN_PROGRESS] } }, + { paymentStatus: PaymentStatus.COMPLETED, condition: { hasWorkPeriodPaymentStatus: [WorkPeriodPaymentStatus.COMPLETED], hasDueDays: false } }, + { paymentStatus: PaymentStatus.PARTIALLY_COMPLETED, condition: { hasWorkPeriodPaymentStatus: [WorkPeriodPaymentStatus.COMPLETED], hasDueDays: true } }, + { paymentStatus: PaymentStatus.FAILED, condition: { hasWorkPeriodPaymentStatus: [PaymentStatus.FAILED], hasDueDays: true } }, + { paymentStatus: PaymentStatus.PENDING, condition: { hasDueDays: true } } + ] + // find the first rule which is matched by the Work Period + for (const rule of PAYMENT_STATUS_RULES) { + if (matchRule(rule)) { + return rule.paymentStatus + } + } + throw new errors.ConflictError('Cannot calculate payment status.') +} + /** * Returns the email address of specified (via handle) user. * @@ -1961,6 +2000,7 @@ module.exports = { createChallengeResource, getChallengeResource, extractWorkPeriods, + calculateWorkPeriodPaymentStatus, getUserByHandle, substituteStringByObject, createProject, diff --git a/src/eventHandlers/ResourceBookingEventHandler.js b/src/eventHandlers/ResourceBookingEventHandler.js index 3ecbbc02..71988593 100644 --- a/src/eventHandlers/ResourceBookingEventHandler.js +++ b/src/eventHandlers/ResourceBookingEventHandler.js @@ -178,13 +178,13 @@ async function updateWorkPeriods (payload) { // find which workperiods should be created const workPeriodsToAdd = _.differenceBy(newWorkPeriods, workPeriods, 'startDate') // find which workperiods' daysWorked property should be evaluated for changes - const IntersectedWorkPeriods = _.intersectionBy(newWorkPeriods, workPeriods, 'startDate') + const intersectedWorkPeriods = _.intersectionBy(newWorkPeriods, workPeriods, 'startDate') let workPeriodsToUpdate = [] - if (IntersectedWorkPeriods.length > 0) { + if (intersectedWorkPeriods.length > 0) { // We only need check for first and last ones of intersected workPeriods // The ones at the middle won't be updated and their daysWorked value will stay the same if (payload.options.oldValue.startDate !== payload.value.startDate) { - const firstWeek = _.minBy(IntersectedWorkPeriods, 'startDate') + const firstWeek = _.minBy(intersectedWorkPeriods, 'startDate') const originalFirstWeek = _.find(workPeriods, ['startDate', firstWeek.startDate]) const existentFirstWeek = _.minBy(workPeriods, 'startDate') // recalculate daysWorked for the first week of existent workPeriods and daysWorked have changed @@ -197,7 +197,7 @@ async function updateWorkPeriods (payload) { } } if (payload.options.oldValue.endDate !== payload.value.endDate) { - const lastWeek = _.maxBy(IntersectedWorkPeriods, 'startDate') + const lastWeek = _.maxBy(intersectedWorkPeriods, 'startDate') const originalLastWeek = _.find(workPeriods, ['startDate', lastWeek.startDate]) const existentLastWeek = _.maxBy(workPeriods, 'startDate') // recalculate daysWorked for the last week of existent workPeriods and daysWorked have changed diff --git a/src/eventHandlers/WorkPeriodPaymentEventHandler.js b/src/eventHandlers/WorkPeriodPaymentEventHandler.js index 0da5163e..9c4d7901 100644 --- a/src/eventHandlers/WorkPeriodPaymentEventHandler.js +++ b/src/eventHandlers/WorkPeriodPaymentEventHandler.js @@ -7,7 +7,7 @@ const config = require('config') const models = require('../models') const logger = require('../common/logger') const helper = require('../common/helper') -const { PaymentStatus, WorkPeriodPaymentStatus } = require('../../app-constants') +const { WorkPeriodPaymentStatus } = require('../../app-constants') const WorkPeriod = models.WorkPeriod /** @@ -22,29 +22,15 @@ async function updateWorkPeriod (payload) { const workPeriodModel = await WorkPeriod.findById(workPeriodPayment.workPeriodId, { withPayments: true }) const workPeriod = workPeriodModel.toJSON() const data = {} - const paymentStatuses = {} data.daysPaid = 0 data.paymentTotal = 0 _.each(workPeriod.payments, payment => { - paymentStatuses[payment.status] = true if (_.includes([WorkPeriodPaymentStatus.SCHEDULED, WorkPeriodPaymentStatus.IN_PROGRESS, WorkPeriodPaymentStatus.COMPLETED], payment.status)) { data.daysPaid += payment.days data.paymentTotal += payment.amount } }) - if (workPeriod.daysWorked === 0) { - data.paymentStatus = PaymentStatus.NO_DAYS - } else if (paymentStatuses[WorkPeriodPaymentStatus.SCHEDULED] || paymentStatuses[WorkPeriodPaymentStatus.IN_PROGRESS]) { - data.paymentStatus = PaymentStatus.IN_PROGRESS - } else if (workPeriod.daysWorked === data.daysPaid) { - data.paymentStatus = PaymentStatus.COMPLETED - } else if (paymentStatuses[WorkPeriodPaymentStatus.COMPLETED]) { - data.paymentStatus = PaymentStatus.PARTIALLY_COMPLETED - } else if (paymentStatuses[WorkPeriodPaymentStatus.FAILED]) { - data.paymentStatus = PaymentStatus.FAILED - } else { - data.paymentStatus = PaymentStatus.PENDING - } + data.paymentStatus = helper.calculateWorkPeriodPaymentStatus(_.assign({}, workPeriod, data)) if (workPeriod.daysPaid === data.daysPaid && workPeriod.paymentTotal === data.paymentTotal && workPeriod.paymentStatus === data.paymentStatus) { logger.debug({ component: 'WorkPeriodPaymentEventHandler', diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index cb70b01f..e3c70e5b 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -171,7 +171,7 @@ async function _ensurePaidWorkPeriodsNotDeleted (resourceBookingId, oldValue, ne function _checkForPaidWorkPeriods (workPeriods) { const paidWorkPeriods = _.filter(workPeriods, workPeriod => { // filter by WP and WPP status - return ([constants.PaymentStatus.COMPLETED, constants.PaymentStatus.PARTIALLY_COMPLETED, constants.PaymentStatus.IN_PROGRESS].indexOf(workPeriod.paymentStatus) !== -1 || + return (workPeriod.daysPaid > 0 || _.some(workPeriod.payments, payment => [constants.WorkPeriodPaymentStatus.COMPLETED, constants.WorkPeriodPaymentStatus.IN_PROGRESS, constants.WorkPeriodPaymentStatus.SCHEDULED].indexOf(payment.status) !== -1)) }) if (paidWorkPeriods.length > 0) { diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index a174058a..ef41c02c 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -259,7 +259,7 @@ async function updateWorkPeriod (currentUser, id, data) { // check permission await _checkUserPermissionForWriteWorkPeriod(currentUser) - const workPeriod = await WorkPeriod.findById(id) + const workPeriod = await WorkPeriod.findById(id, { withPayments: true }) const oldValue = workPeriod.toJSON() if (oldValue.daysWorked === data.daysWorked) { return oldValue @@ -279,14 +279,9 @@ async function updateWorkPeriod (currentUser, id, data) { if (thisWeek.daysWorked < data.daysWorked) { throw new errors.BadRequestError(`Maximum allowed daysWorked is (${thisWeek.daysWorked})`) } - if (data.daysWorked > oldValue.daysWorked && oldValue.paymentStatus === constants.PaymentStatus.COMPLETED) { - data.paymentStatus = constants.PaymentStatus.PARTIALLY_COMPLETED - } else if (data.daysWorked > oldValue.daysWorked && oldValue.paymentStatus === constants.PaymentStatus.NO_DAYS) { - data.paymentStatus = constants.PaymentStatus.PENDING - } else if (data.daysWorked === oldValue.daysPaid && _.includes([constants.PaymentStatus.PARTIALLY_COMPLETED, constants.PaymentStatus.FAILED], oldValue.paymentStatus)) { - data.paymentStatus = constants.PaymentStatus.COMPLETED - } else if (data.daysWorked === 0) { - data.paymentStatus = constants.PaymentStatus.NO_DAYS + data.paymentStatus = helper.calculateWorkPeriodPaymentStatus(_.assign({}, oldValue, data)) + if (oldValue.paymentStatus === data.paymentStatus) { + return oldValue } data.updatedBy = await helper.getUserId(currentUser.userId) const updated = await workPeriod.update(data) diff --git a/test/unit/common/WorkPeriodPaymentData.js b/test/unit/common/WorkPeriodPaymentData.js index 6c54e525..67322137 100644 --- a/test/unit/common/WorkPeriodPaymentData.js +++ b/test/unit/common/WorkPeriodPaymentData.js @@ -12,7 +12,7 @@ const workPeriodPayment01 = { customerRate: 13, id: '01971e6f-0f09-4a2a-bc2e-2adac0f00622', challengeId: '00000000-0000-0000-0000-000000000000', - createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + createdBy: '79a39efd-91af-494a-b0f6-62310495effd', updatedAt: '2021-04-21T12:58:07.535Z', createdAt: '2021-04-21T12:58:07.535Z', updatedBy: null @@ -39,7 +39,7 @@ const workPeriodPayment01 = { userHandle: 'pshah_manager', updatedBy: null, endDate: '2020-10-31', - daysPaid: 5, + daysPaid: 2, resourceBookingId: '8694a939-45fe-482e-bee2-3b530acf4139', daysWorked: 5, createdAt: '2021-06-13T18:21:52.564Z', @@ -48,7 +48,7 @@ const workPeriodPayment01 = { id: '467b4df7-ced4-41b9-9710-b83808cddaf4', projectId: 17234, startDate: '2020-10-25', - paymentStatus: 'in-progress', + paymentStatus: 'partially-completed', updatedAt: '2021-06-13T18:25:08.492Z', payments: [ { @@ -75,11 +75,11 @@ const workPeriodPayment01 = { createdAt: '2021-06-13T18:22:10.258Z', challengeId: '00000000-0000-0000-0000-000000000000', memberRate: 13.23, - createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + createdBy: '79a39efd-91af-494a-b0f6-62310495effd', customerRate: 13, days: 3, statusDetails: null, - id: '2a30b5a1-3558-4795-b516-d03cb098fc0f', + id: '01971e6f-0f09-4a2a-bc2e-2adac0f00622', status: 'scheduled', updatedAt: '2021-06-13T18:25:08.445Z' } From 015a796f63ab48c7c1ffcaa4d29729c0568f4eaa Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 16 Jun 2021 03:12:41 +0300 Subject: [PATCH 79/86] fix: project creation & missing job data --- src/common/helper.js | 6 ++--- src/services/TeamService.js | 44 ++++++++++++++++++------------------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 45e4afe4..4b536f10 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1867,11 +1867,11 @@ async function getTags (description) { * @param {Object} data title of project and any other info * @returns {Object} the project created */ -async function createProject (data) { - const token = await getM2MToken() +async function createProject (currentUser, data) { + const token = currentUser.jwtToken const res = await request .post(`${config.TC_API}/projects/`) - .set('Authorization', `Bearer ${token}`) + .set('Authorization', token) .set('Content-Type', 'application/json') .set('Accept', 'application/json') .send(data) diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 566262a3..02135f64 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -918,7 +918,6 @@ async function getSkillIdsByNames (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 } @@ -1014,19 +1013,13 @@ async function createTeam (currentUser, data) { const projectRequestBody = { name: data.teamName, description: data.teamDescription, - type: 'app_dev', + type: 'talent-as-a-service', 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 }) - } + const project = await helper.createProject(currentUser, projectRequestBody) // create jobs for the given positions. await Promise.all(_.map(data.positions, async position => { const roleSearchRequest = roleSearchRequests[position.roleSearchRequestId] @@ -1034,11 +1027,12 @@ async function createTeam (currentUser, data) { projectId: project.id, title: position.roleName, numPositions: position.numberOfResources, - rateType: 'weekly', - skills: roleSearchRequest.skills - } - if (roleSearchRequest.jobDescription) { - job.description = roleSearchRequest.jobDescription + rateType: position.rateType, + workload: position.workload, + skills: roleSearchRequest.skills, + description: roleSearchRequest.jobDescription, + roleIds: [roleSearchRequest.roleId], + resourceType: roleSearchRequest.resourceType } if (position.startMonth) { job.startDate = position.startMonth @@ -1046,9 +1040,6 @@ async function createTeam (currentUser, data) { if (position.durationWeeks) { job.duration = position.durationWeeks } - if (roleSearchRequest.roleId) { - job.roleIds = [roleSearchRequest.roleId] - } await JobService.createJob(currentUser, job) })) return { projectId: project.id } @@ -1066,7 +1057,10 @@ createTeam.schema = Joi.object() roleSearchRequestId: Joi.string().uuid().required(), numberOfResources: Joi.number().integer().min(1).required(), durationWeeks: Joi.number().integer().min(1), - startMonth: Joi.date() + startMonth: Joi.date(), + rateType: Joi.rateType().default('weekly'), + workload: Joi.workload().default('full-time'), + resourceType: Joi.string() }).required() ).required() }).required() @@ -1084,19 +1078,23 @@ async function _validateRoleSearchRequests (roleSearchRequestIds) { 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`) + // we can't create a job without a role + if (!roleSearchRequest.roleId) { + throw new errors.ConflictError(`roleSearchRequestId: ${roleSearchRequestId} must have roleId`) } + const role = await Role.findById(roleSearchRequest.roleId) // 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 + // store role's skills roleSearchRequests[roleSearchRequestId].skills = await getSkillIdsByNames(role.listOfSkills) } + if (!roleSearchRequest.jobDescription) { + roleSearchRequests[roleSearchRequestId].jobDescription = role.description + } + roleSearchRequests[roleSearchRequestId].resourceType = role.name })) return roleSearchRequests } From e68e4fca802a82ba3f5fc3375418f0e9f2dccaed Mon Sep 17 00:00:00 2001 From: eisbilir <72204071+eisbilir@users.noreply.github.com> Date: Wed, 16 Jun 2021 10:45:39 +0300 Subject: [PATCH 80/86] fix: workPeriod update --- src/services/WorkPeriodService.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index ef41c02c..af4855da 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -280,9 +280,6 @@ async function updateWorkPeriod (currentUser, id, data) { throw new errors.BadRequestError(`Maximum allowed daysWorked is (${thisWeek.daysWorked})`) } data.paymentStatus = helper.calculateWorkPeriodPaymentStatus(_.assign({}, oldValue, data)) - if (oldValue.paymentStatus === data.paymentStatus) { - return oldValue - } data.updatedBy = await helper.getUserId(currentUser.userId) const updated = await workPeriod.update(data) await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue, key: `resourceBooking.id:${updated.resourceBookingId}` }) From 2d2a3b82a8af63f4ac71035409bd4b1df4126525 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 16 Jun 2021 11:00:56 +0300 Subject: [PATCH 81/86] chore: user 10 partions for Kafka events locally --- local/kafka-client/create-topics.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local/kafka-client/create-topics.sh b/local/kafka-client/create-topics.sh index 88339fe9..f82c65e9 100755 --- a/local/kafka-client/create-topics.sh +++ b/local/kafka-client/create-topics.sh @@ -2,5 +2,5 @@ /opt/kafka/bin/kafka-topics.sh --list --zookeeper zookeeper:2181 > exists-topics.txt while read topic; do - /opt/kafka/bin/kafka-topics.sh --create --if-not-exists --zookeeper zookeeper:2181 --partitions 1 --replication-factor 1 --topic $topic + /opt/kafka/bin/kafka-topics.sh --create --if-not-exists --zookeeper zookeeper:2181 --partitions 10 --replication-factor 1 --topic $topic done < <(sort topics.txt exists-topics.txt exists-topics.txt | uniq -u) \ No newline at end of file From 73c1efcd81925c44dda238789a18522226d23911 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 16 Jun 2021 11:01:36 +0300 Subject: [PATCH 82/86] fix: WP update endpoint fixes --- src/services/WorkPeriodPaymentService.js | 3 --- src/services/WorkPeriodService.js | 9 ++++----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index ce1e016d..f68428eb 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -203,9 +203,6 @@ async function updateWorkPeriodPayment (currentUser, id, data) { const workPeriodPayment = await WorkPeriodPayment.findById(id) const oldValue = workPeriodPayment.toJSON() - if (oldValue.status === data.status) { - return oldValue - } if (data.status === 'cancelled' && oldValue.status === 'in-progress') { throw new errors.BadRequestError('You cannot cancel a WorkPeriodPayment which is in-progress') } diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index af4855da..834ea4e0 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -261,9 +261,6 @@ async function updateWorkPeriod (currentUser, id, data) { const workPeriod = await WorkPeriod.findById(id, { withPayments: true }) const oldValue = workPeriod.toJSON() - if (oldValue.daysWorked === data.daysWorked) { - return oldValue - } if (data.daysWorked < oldValue.daysPaid) { throw new errors.BadRequestError(`Cannot update daysWorked (${data.daysWorked}) to the value less than daysPaid (${oldValue.daysPaid})`) } @@ -282,8 +279,10 @@ async function updateWorkPeriod (currentUser, id, data) { data.paymentStatus = helper.calculateWorkPeriodPaymentStatus(_.assign({}, oldValue, data)) data.updatedBy = await helper.getUserId(currentUser.userId) const updated = await workPeriod.update(data) - await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue, key: `resourceBooking.id:${updated.resourceBookingId}` }) - return updated.dataValues + const updatedDataWithoutPayments = _.omit(updated.toJSON(), ['payments']) + const oldValueWithoutPayments = _.omit(oldValue, ['payments']) + await helper.postEvent(config.TAAS_WORK_PERIOD_UPDATE_TOPIC, updatedDataWithoutPayments, { oldValue: oldValueWithoutPayments, key: `resourceBooking.id:${updated.resourceBookingId}` }) + return updatedDataWithoutPayments } /** From 09d363675eb5eabbcd5c2afe0972c58f46e3fe87 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 16 Jun 2021 11:02:04 +0300 Subject: [PATCH 83/86] fix: only retry erros with code 500 --- src/services/PaymentSchedulerService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/PaymentSchedulerService.js b/src/services/PaymentSchedulerService.js index 20ebf249..666c66bb 100644 --- a/src/services/PaymentSchedulerService.js +++ b/src/services/PaymentSchedulerService.js @@ -282,7 +282,7 @@ async function checkPerMinThreshold (key) { * @returns {Boolean} */ function validateError (err) { - return !err.status || err.status >= 500 + return err.status >= 500 && err.status < 600 } /** From 52469f377b2af39faa44d5cebf0f01cb04aa0721 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 16 Jun 2021 11:05:03 +0300 Subject: [PATCH 84/86] feate: sort WP inside RB by startDate for comfort --- src/services/ResourceBookingService.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index e3c70e5b..9ca27bc5 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -613,6 +613,13 @@ async function searchResourceBookings (currentUser, criteria, options = { return }) }) + // sort Work Periods inside Resource Bookings by startDate just for comfort output + _.each(resourceBookings, r => { + if (_.isArray(r.workPeriods)) { + r.workPeriods = _.sortBy(r.workPeriods, ['startDate']) + } + }) + return { total: body.hits.total.value, page, @@ -676,6 +683,12 @@ async function searchResourceBookings (currentUser, criteria, options = { return queryCriteria.order = [[{ model: WorkPeriod, as: 'workPeriods' }, _.split(criteria.sortBy, '.')[1], `${criteria.sortOrder} NULLS LAST`]] } const result = await ResourceBooking.findAll(queryCriteria) + // sort Work Periods inside Resource Bookings by startDate just for comfort output + _.each(result, r => { + if (_.isArray(r.workPeriods)) { + r.workPeriods = _.sortBy(r.workPeriods, ['startDate']) + } + }) let countQuery countQuery = _.omit(queryCriteria, ['limit', 'offset', 'attributes', 'order']) if (queryOpt.withWorkPeriods && !queryCriteria.include[0].required) { From b0242d8b1312052e42425fa663a793ca0238ce4f Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 16 Jun 2021 13:29:41 +0300 Subject: [PATCH 85/86] refactor: various payment code improvements --- app-constants.js | 42 +++++++- ...ate-work-periods-for-resource-bookings.js} | 0 src/bootstrap.js | 4 +- src/common/helper.js | 13 +-- .../ResourceBookingEventHandler.js | 4 +- .../WorkPeriodPaymentEventHandler.js | 4 +- src/services/ResourceBookingService.js | 7 +- src/services/WorkPeriodPaymentService.js | 6 +- src/services/WorkPeriodService.js | 7 +- test/unit/common/ResourceBookingData.js | 98 ++++++++++++++++--- 10 files changed, 137 insertions(+), 48 deletions(-) rename migrations/{2021-06-14-create-and-populate-work-periods-for-resource-bookings.js => 2021-06-14-recreate-and-repopulate-work-periods-for-resource-bookings.js} (100%) diff --git a/app-constants.js b/app-constants.js index a09f57aa..83dcbdb2 100644 --- a/app-constants.js +++ b/app-constants.js @@ -84,15 +84,21 @@ const ChallengeStatus = { COMPLETED: 'Completed' } -const PaymentStatus = { +/** + * Aggregate payment status for Work Period which is determined + * based on the payments the Work Period has using `PaymentStatusRules` + */ +const AggregatePaymentStatus = { PENDING: 'pending', IN_PROGRESS: 'in-progress', PARTIALLY_COMPLETED: 'partially-completed', COMPLETED: 'completed', - FAILED: 'failed', NO_DAYS: 'no-days' } +/** + * `WorkPeriodPayment.status` - possible values + */ const WorkPeriodPaymentStatus = { COMPLETED: 'completed', SCHEDULED: 'scheduled', @@ -101,6 +107,32 @@ const WorkPeriodPaymentStatus = { CANCELLED: 'cancelled' } +/** + * The rules how to determine WorkPeriod.paymentStatus based on the payments + * + * The top rule has priority over the bottom rules. + */ +const PaymentStatusRules = [ + { paymentStatus: AggregatePaymentStatus.NO_DAYS, condition: { daysWorked: 0 } }, + { paymentStatus: AggregatePaymentStatus.IN_PROGRESS, condition: { hasWorkPeriodPaymentStatus: [WorkPeriodPaymentStatus.SCHEDULED, WorkPeriodPaymentStatus.IN_PROGRESS] } }, + { paymentStatus: AggregatePaymentStatus.COMPLETED, condition: { hasWorkPeriodPaymentStatus: [WorkPeriodPaymentStatus.COMPLETED], hasDueDays: false } }, + { paymentStatus: AggregatePaymentStatus.PARTIALLY_COMPLETED, condition: { hasWorkPeriodPaymentStatus: [WorkPeriodPaymentStatus.COMPLETED], hasDueDays: true } }, + { paymentStatus: AggregatePaymentStatus.PENDING, condition: { hasDueDays: true } } +] + +/** + * The WorkPeriodPayment.status values which we take into account when calculate + * aggregate values inside WorkPeriod: + * - daysPaid + * - paymentTotal + * - paymentStatus + */ +const ActiveWorkPeriodPaymentStatuses = [ + WorkPeriodPaymentStatus.SCHEDULED, + WorkPeriodPaymentStatus.IN_PROGRESS, + WorkPeriodPaymentStatus.COMPLETED +] + const WorkPeriodPaymentUpdateStatus = { SCHEDULED: 'scheduled', CANCELLED: 'cancelled' @@ -126,9 +158,11 @@ module.exports = { Scopes, Interviews, ChallengeStatus, - PaymentStatus, + AggregatePaymentStatus, WorkPeriodPaymentStatus, WorkPeriodPaymentUpdateStatus, PaymentSchedulerStatus, - PaymentProcessingSwitch + PaymentProcessingSwitch, + PaymentStatusRules, + ActiveWorkPeriodPaymentStatuses } diff --git a/migrations/2021-06-14-create-and-populate-work-periods-for-resource-bookings.js b/migrations/2021-06-14-recreate-and-repopulate-work-periods-for-resource-bookings.js similarity index 100% rename from migrations/2021-06-14-create-and-populate-work-periods-for-resource-bookings.js rename to migrations/2021-06-14-recreate-and-repopulate-work-periods-for-resource-bookings.js diff --git a/src/bootstrap.js b/src/bootstrap.js index b2f094b0..6e088046 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -3,7 +3,7 @@ const Joi = require('joi') const config = require('config') const path = require('path') const _ = require('lodash') -const { Interviews, PaymentStatus, WorkPeriodPaymentStatus, WorkPeriodPaymentUpdateStatus, PaymentProcessingSwitch } = require('../app-constants') +const { Interviews, AggregatePaymentStatus, WorkPeriodPaymentStatus, WorkPeriodPaymentUpdateStatus, PaymentProcessingSwitch } = require('../app-constants') const logger = require('./common/logger') const allowedInterviewStatuses = _.values(Interviews.Status) @@ -17,7 +17,7 @@ Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancel Joi.workload = () => Joi.string().valid('full-time', 'fractional') Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed', 'offered') Joi.title = () => Joi.string().max(128) -Joi.paymentStatus = () => Joi.string().valid(..._.values(PaymentStatus)) +Joi.paymentStatus = () => Joi.string().valid(..._.values(AggregatePaymentStatus)) Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) Joi.interviewStatus = () => Joi.string().valid(...allowedInterviewStatuses) Joi.workPeriodPaymentStatus = () => Joi.string().valid(..._.values(WorkPeriodPaymentStatus)) diff --git a/src/common/helper.js b/src/common/helper.js index a77a0561..773dde0f 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -21,7 +21,7 @@ const models = require('../models') const eventDispatcher = require('./eventDispatcher') const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') const moment = require('moment') -const { PaymentStatus, WorkPeriodPaymentStatus } = require('../../app-constants') +const { PaymentStatusRules } = require('../../app-constants') const localLogger = { debug: (message) => @@ -1829,17 +1829,8 @@ function calculateWorkPeriodPaymentStatus (workPeriod) { } }) } - // define rules for the payment status as constant - const PAYMENT_STATUS_RULES = [ - { paymentStatus: PaymentStatus.NO_DAYS, condition: { daysWorked: 0 } }, - { paymentStatus: PaymentStatus.IN_PROGRESS, condition: { hasWorkPeriodPaymentStatus: [WorkPeriodPaymentStatus.SCHEDULED, WorkPeriodPaymentStatus.IN_PROGRESS] } }, - { paymentStatus: PaymentStatus.COMPLETED, condition: { hasWorkPeriodPaymentStatus: [WorkPeriodPaymentStatus.COMPLETED], hasDueDays: false } }, - { paymentStatus: PaymentStatus.PARTIALLY_COMPLETED, condition: { hasWorkPeriodPaymentStatus: [WorkPeriodPaymentStatus.COMPLETED], hasDueDays: true } }, - { paymentStatus: PaymentStatus.FAILED, condition: { hasWorkPeriodPaymentStatus: [PaymentStatus.FAILED], hasDueDays: true } }, - { paymentStatus: PaymentStatus.PENDING, condition: { hasDueDays: true } } - ] // find the first rule which is matched by the Work Period - for (const rule of PAYMENT_STATUS_RULES) { + for (const rule of PaymentStatusRules) { if (matchRule(rule)) { return rule.paymentStatus } diff --git a/src/eventHandlers/ResourceBookingEventHandler.js b/src/eventHandlers/ResourceBookingEventHandler.js index 71988593..1134674a 100644 --- a/src/eventHandlers/ResourceBookingEventHandler.js +++ b/src/eventHandlers/ResourceBookingEventHandler.js @@ -7,7 +7,7 @@ const _ = require('lodash') const models = require('../models') const logger = require('../common/logger') const helper = require('../common/helper') -const { PaymentStatus } = require('../../app-constants') +const { AggregatePaymentStatus } = require('../../app-constants') const JobService = require('../services/JobService') const JobCandidateService = require('../services/JobCandidateService') const WorkPeriodService = require('../services/WorkPeriodService') @@ -296,7 +296,7 @@ async function _createWorkPeriods (periods, resourceBookingId) { startDate: period.startDate, endDate: period.endDate, daysWorked: period.daysWorked, - paymentStatus: period.daysWorked === 0 ? PaymentStatus.NO_DAYS : PaymentStatus.PENDING + paymentStatus: period.daysWorked === 0 ? AggregatePaymentStatus.NO_DAYS : AggregatePaymentStatus.PENDING }) } } diff --git a/src/eventHandlers/WorkPeriodPaymentEventHandler.js b/src/eventHandlers/WorkPeriodPaymentEventHandler.js index 9c4d7901..53ab7823 100644 --- a/src/eventHandlers/WorkPeriodPaymentEventHandler.js +++ b/src/eventHandlers/WorkPeriodPaymentEventHandler.js @@ -7,7 +7,7 @@ const config = require('config') const models = require('../models') const logger = require('../common/logger') const helper = require('../common/helper') -const { WorkPeriodPaymentStatus } = require('../../app-constants') +const { ActiveWorkPeriodPaymentStatuses } = require('../../app-constants') const WorkPeriod = models.WorkPeriod /** @@ -25,7 +25,7 @@ async function updateWorkPeriod (payload) { data.daysPaid = 0 data.paymentTotal = 0 _.each(workPeriod.payments, payment => { - if (_.includes([WorkPeriodPaymentStatus.SCHEDULED, WorkPeriodPaymentStatus.IN_PROGRESS, WorkPeriodPaymentStatus.COMPLETED], payment.status)) { + if (_.includes(ActiveWorkPeriodPaymentStatuses, payment.status)) { data.daysPaid += payment.days data.paymentTotal += payment.amount } diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 9ca27bc5..9204eed5 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -171,12 +171,11 @@ async function _ensurePaidWorkPeriodsNotDeleted (resourceBookingId, oldValue, ne function _checkForPaidWorkPeriods (workPeriods) { const paidWorkPeriods = _.filter(workPeriods, workPeriod => { // filter by WP and WPP status - return (workPeriod.daysPaid > 0 || - _.some(workPeriod.payments, payment => [constants.WorkPeriodPaymentStatus.COMPLETED, constants.WorkPeriodPaymentStatus.IN_PROGRESS, constants.WorkPeriodPaymentStatus.SCHEDULED].indexOf(payment.status) !== -1)) + return _.some(workPeriod.payments, payment => constants.ActiveWorkPeriodPaymentStatuses.indexOf(payment.status) !== -1) }) if (paidWorkPeriods.length > 0) { - throw new errors.BadRequestError(`WorkPeriods with id of ${_.map(paidWorkPeriods, workPeriod => workPeriod.id)} - has ${constants.PaymentStatus.COMPLETED}, ${constants.PaymentStatus.PARTIALLY_COMPLETED} or ${constants.PaymentStatus.IN_PROGRESS} payment status.`) + throw new errors.BadRequestError(`Can't delete associated WorkPeriods ${_.map(paidWorkPeriods, workPeriod => workPeriod.id)} + as they have associated WorkPeriodsPayment with one of statuses ${constants.ActiveWorkPeriodPaymentStatuses.join(', ')}.`) } } // find related workPeriods to evaluate the changes diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index f68428eb..576d934d 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -12,7 +12,7 @@ const helper = require('../common/helper') const logger = require('../common/logger') const errors = require('../common/errors') const models = require('../models') -const { PaymentStatus, WorkPeriodPaymentStatus } = require('../../app-constants') +const { WorkPeriodPaymentStatus } = require('../../app-constants') const { searchResourceBookings } = require('./ResourceBookingService') const WorkPeriodPayment = models.WorkPeriodPayment @@ -382,7 +382,7 @@ async function createQueryWorkPeriodPayments (currentUser, criteria) { const createdBy = await helper.getUserId(currentUser.userId) const query = criteria.query if ((typeof query['workPeriods.paymentStatus']) === 'string') { - query['workPeriods.paymentStatus'] = query['workPeriods.paymentStatus'].trim().split(',').map(ps => Joi.attempt({ paymentStatus: ps.trim() }, Joi.object().keys({ paymentStatus: Joi.string().valid(PaymentStatus.PENDING, PaymentStatus.PARTIALLY_COMPLETED, PaymentStatus.FAILED) })).paymentStatus) + query['workPeriods.paymentStatus'] = query['workPeriods.paymentStatus'].trim().split(',').map(ps => Joi.attempt({ paymentStatus: ps.trim() }, Joi.object().keys({ paymentStatus: Joi.paymentStatus() })).paymentStatus) } const fields = _.join(_.uniq(_.concat( ['id', 'billingAccountId', 'memberRate', 'customerRate', 'workPeriods.id', 'workPeriods.resourceBookingId', 'workPeriods.daysWorked', 'workPeriods.daysPaid'], @@ -423,7 +423,7 @@ createQueryWorkPeriodPayments.schema = Joi.object().keys({ ), 'workPeriods.paymentStatus': Joi.alternatives( Joi.string(), - Joi.array().items(Joi.string().valid(PaymentStatus.PENDING, PaymentStatus.PARTIALLY_COMPLETED, PaymentStatus.FAILED)) + Joi.array().items(Joi.string().valid(Joi.paymentStatus())) ), 'workPeriods.startDate': Joi.date().format('YYYY-MM-DD'), 'workPeriods.endDate': Joi.date().format('YYYY-MM-DD'), diff --git a/src/services/WorkPeriodService.js b/src/services/WorkPeriodService.js index 834ea4e0..8d018fb0 100644 --- a/src/services/WorkPeriodService.js +++ b/src/services/WorkPeriodService.js @@ -310,11 +310,8 @@ partiallyUpdateWorkPeriod.schema = Joi.object().keys({ */ async function deleteWorkPeriod (id) { const workPeriod = await WorkPeriod.findById(id, { withPayments: true }) - if (_.includes([constants.PaymentStatus.COMPLETED, constants.PaymentStatus.PARTIALLY_COMPLETED, constants.PaymentStatus.IN_PROGRESS], workPeriod.paymentStatus)) { - throw new errors.BadRequestError(`Can't delete WorkPeriod with paymentStatus ${constants.PaymentStatus.COMPLETED}, ${constants.PaymentStatus.PARTIALLY_COMPLETED}, or ${constants.PaymentStatus.IN_PROGRESS}`) - } - if (_.some(workPeriod.payments, payment => [constants.WorkPeriodPaymentStatus.COMPLETED, constants.WorkPeriodPaymentStatus.IN_PROGRESS, constants.WorkPeriodPaymentStatus.SCHEDULED].indexOf(payment.status) !== -1)) { - throw new errors.BadRequestError(`Can't delete WorkPeriod if any associated WorkPeriodsPayment has status ${constants.WorkPeriodPaymentStatus.COMPLETED}, ${constants.WorkPeriodPaymentStatus.SCHEDULED} or ${constants.WorkPeriodPaymentStatus.IN_PROGRESS}`) + if (_.some(workPeriod.payments, payment => constants.ActiveWorkPeriodPaymentStatuses.indexOf(payment.status) !== -1)) { + throw new errors.BadRequestError(`Can't delete WorkPeriod as it has associated WorkPeriodsPayment with one of statuses ${constants.ActiveWorkPeriodPaymentStatuses.join(', ')}`) } await models.WorkPeriodPayment.destroy({ where: { diff --git a/test/unit/common/ResourceBookingData.js b/test/unit/common/ResourceBookingData.js index 67cfbd9e..0970c73e 100644 --- a/test/unit/common/ResourceBookingData.js +++ b/test/unit/common/ResourceBookingData.js @@ -842,8 +842,8 @@ const T13 = { }, error: { httpStatus: 400, - message: `WorkPeriods with id of 10faf505-d0e3-4d13-a817-7f1319625e91 - has completed, partially-completed or in-progress payment status.` + message: `Can't delete associated WorkPeriods 10faf505-d0e3-4d13-a817-7f1319625e91 + as they have associated WorkPeriodsPayment with one of statuses scheduled, in-progress, completed.` }, workPeriod: { response: [{ @@ -861,7 +861,8 @@ const T13 = { createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + updatedAt: '2021-04-10T22:25:08.289Z', + payments: [] }, toJSON: () => T13.workPeriod.response[0].dataValues }, { @@ -879,7 +880,23 @@ const T13 = { createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + updatedAt: '2021-04-10T22:25:08.289Z', + payments: [{ + amount: 400, + updatedBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + billingAccountId: 80000071, + workPeriodId: '193a029e-0d3d-48fa-a249-0a4b6b21c32a', + createdAt: '2021-06-16T08:51:30.374Z', + challengeId: '3c849caf-6c60-40f7-8f0e-ee72c41ede6e', + memberRate: 2000, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 2600, + days: 1, + statusDetails: null, + id: 'c3ba01c5-56db-462f-811f-65cd256096c3', + status: 'in-progress', + updatedAt: '2021-06-16T08:57:21.483Z' + }] }, toJSON: () => T13.workPeriod.response[1].dataValues }] @@ -912,8 +929,8 @@ const T14 = { }, error: { httpStatus: 400, - message: `WorkPeriods with id of 10faf505-d0e3-4d13-a817-7f1319625e91 - has completed, partially-completed or in-progress payment status.` + message: `Can't delete associated WorkPeriods 10faf505-d0e3-4d13-a817-7f1319625e91 + as they have associated WorkPeriodsPayment with one of statuses scheduled, in-progress, completed.` }, workPeriod: { response: [{ @@ -931,7 +948,8 @@ const T14 = { createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + updatedAt: '2021-04-10T22:25:08.289Z', + payments: [] }, toJSON: () => T14.workPeriod.response[0].dataValues }, { @@ -949,7 +967,23 @@ const T14 = { createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + updatedAt: '2021-04-10T22:25:08.289Z', + payments: [{ + amount: 400, + updatedBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + billingAccountId: 80000071, + workPeriodId: '193a029e-0d3d-48fa-a249-0a4b6b21c32a', + createdAt: '2021-06-16T08:51:30.374Z', + challengeId: '3c849caf-6c60-40f7-8f0e-ee72c41ede6e', + memberRate: 2000, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 2600, + days: 1, + statusDetails: null, + id: 'c3ba01c5-56db-462f-811f-65cd256096c3', + status: 'scheduled', + updatedAt: '2021-06-16T08:57:21.483Z' + }] }, toJSON: () => T14.workPeriod.response[1].dataValues }] @@ -992,7 +1026,8 @@ const T15 = { createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + updatedAt: '2021-04-10T22:25:08.289Z', + payments: [] }, toJSON: () => T15.workPeriod.response[0].dataValues }, { @@ -1010,7 +1045,23 @@ const T15 = { createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + updatedAt: '2021-04-10T22:25:08.289Z', + payments: [{ + amount: 400, + updatedBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + billingAccountId: 80000071, + workPeriodId: '193a029e-0d3d-48fa-a249-0a4b6b21c32a', + createdAt: '2021-06-16T08:51:30.374Z', + challengeId: '3c849caf-6c60-40f7-8f0e-ee72c41ede6e', + memberRate: 2000, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 2600, + days: 1, + statusDetails: null, + id: 'c3ba01c5-56db-462f-811f-65cd256096c3', + status: 'failed', + updatedAt: '2021-06-16T08:57:21.483Z' + }] }, toJSON: () => T15.workPeriod.response[1].dataValues }], @@ -1048,8 +1099,8 @@ const T16 = { }, error: { httpStatus: 400, - message: `WorkPeriods with id of 10faf505-d0e3-4d13-a817-7f1319625e91 - has completed, partially-completed or in-progress payment status.` + message: `Can't delete associated WorkPeriods 10faf505-d0e3-4d13-a817-7f1319625e91 + as they have associated WorkPeriodsPayment with one of statuses scheduled, in-progress, completed.` }, workPeriod: { response: [{ @@ -1067,7 +1118,8 @@ const T16 = { createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + updatedAt: '2021-04-10T22:25:08.289Z', + payments: [] }, toJSON: () => T16.workPeriod.response[0].dataValues }, { @@ -1080,12 +1132,28 @@ const T16 = { endDate: '2021-04-17', daysWorked: 4, daysPaid: 4, - paymentTotal: 10.59, + paymentTotal: 400, paymentStatus: 'completed', createdBy: '00000000-0000-0000-0000-000000000000', updatedBy: null, createdAt: '2021-04-10T22:25:08.289Z', - updatedAt: '2021-04-10T22:25:08.289Z' + updatedAt: '2021-04-10T22:25:08.289Z', + payments: [{ + amount: 400, + updatedBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + billingAccountId: 80000071, + workPeriodId: '193a029e-0d3d-48fa-a249-0a4b6b21c32a', + createdAt: '2021-06-16T08:51:30.374Z', + challengeId: '3c849caf-6c60-40f7-8f0e-ee72c41ede6e', + memberRate: 2000, + createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c', + customerRate: 2600, + days: 4, + statusDetails: null, + id: 'c3ba01c5-56db-462f-811f-65cd256096c3', + status: 'completed', + updatedAt: '2021-06-16T08:57:21.483Z' + }] }, toJSON: () => T16.workPeriod.response[1].dataValues }] From 93f136f311bd340736b0fb9de600325a4123f1bb Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 16 Jun 2021 14:26:34 +0300 Subject: [PATCH 86/86] fix: migration script --- ...reate-and-repopulate-work-periods-for-resource-bookings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/2021-06-14-recreate-and-repopulate-work-periods-for-resource-bookings.js b/migrations/2021-06-14-recreate-and-repopulate-work-periods-for-resource-bookings.js index 7e484f4f..604a2072 100644 --- a/migrations/2021-06-14-recreate-and-repopulate-work-periods-for-resource-bookings.js +++ b/migrations/2021-06-14-recreate-and-repopulate-work-periods-for-resource-bookings.js @@ -3,7 +3,7 @@ const ResourceBooking = require('../src/models').ResourceBooking const _ = require('lodash') const helper = require('../src/common/helper') const { v4: uuid } = require('uuid') -const { PaymentStatus } = require('../app-constants') +const { AggregatePaymentStatus } = require('../app-constants') // maximum start date of resource bookings when populating work periods from existing resource bookings in migration script const MAX_START_DATE = process.env.MAX_START_DATE || '2100-12-31' @@ -85,7 +85,7 @@ module.exports = { days_worked: period.daysWorked, days_paid: 0, payment_total: 0, - payment_status: period.daysWorked === 0 ? PaymentStatus.NO_DAYS : PaymentStatus.PENDING, + payment_status: period.daysWorked === 0 ? AggregatePaymentStatus.NO_DAYS : AggregatePaymentStatus.PENDING, created_by: config.m2m.M2M_AUDIT_USER_ID, created_at: new Date() })